feat: release 2.2.0 with PC-only tests and execution reliability fixes
Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
This commit is contained in:
1 parent
fac4ccb760
commit
be6e8c2abd
79 files changed
+2263
-277
No files matched your search
@@ -89,9 +89,8 @@ class DeviceLockOwner:
|
||||
class DeviceExecutionLock:
|
||||
"""A PID-aware FIFO lease supporting per-device mutexes and global concurrency limits.
|
||||
|
||||
Artemis enforces a single active task per mobile device at any time.
|
||||
Different devices can execute tasks concurrently unless a global concurrency
|
||||
limit (e.g. concurrency_mode="global" or ARTEMIS_MAX_CONCURRENT_TASKS=1) is configured.
|
||||
Artemis 2.2 serializes tasks across Android and computer targets. Legacy
|
||||
concurrency options remain accepted but cannot enable parallel execution.
|
||||
|
||||
Atomic file creation provides exclusion per device serial. PID creation time
|
||||
prevents stale owners or queue tickets from blocking execution after a process
|
||||
@@ -102,6 +101,7 @@ class DeviceExecutionLock:
|
||||
_DEFAULT_POLL_INTERVAL_SECONDS = 0.1
|
||||
QUEUE_TICKET_ENV = "ARTEMIS_DEVICE_QUEUE_TICKET"
|
||||
LOCK_SCOPE_ENV = "ARTEMIS_ADB_ENDPOINT_ID"
|
||||
EXECUTION_RESOURCE = "artemis:execution"
|
||||
|
||||
@staticmethod
|
||||
def _normalize_device_id(device_id: str | None) -> str:
|
||||
@@ -160,7 +160,12 @@ class DeviceExecutionLock:
|
||||
self._queue_path: Path | None = None
|
||||
self._acquired = False
|
||||
self.resource_ids = tuple(
|
||||
sorted(set(r.strip().casefold() for r in resource_ids if r.strip()))
|
||||
sorted(
|
||||
{
|
||||
self.EXECUTION_RESOURCE,
|
||||
*(r.strip().casefold() for r in resource_ids if r.strip()),
|
||||
}
|
||||
)
|
||||
)
|
||||
self._resource_paths: list[Path] = []
|
||||
|
||||
@@ -613,7 +618,15 @@ class DeviceExecutionLock:
|
||||
path = self.path.parent / f"artemis-device-resource-{digest}.lock"
|
||||
# Same resource identity applies across plugins and ADB server endpoints.
|
||||
owner = self._get_active_owner_for_path(path)
|
||||
if owner is not None and owner.token == self.token:
|
||||
if owner is not None and (
|
||||
owner.token == self.token
|
||||
or (
|
||||
resource == self.EXECUTION_RESOURCE
|
||||
and owner.pid == os.getpid()
|
||||
and self.session_id
|
||||
and owner.session_id == self.session_id
|
||||
)
|
||||
):
|
||||
continue
|
||||
try:
|
||||
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
||||
@@ -675,37 +688,17 @@ class DeviceExecutionLock:
|
||||
|
||||
@property
|
||||
def effective_concurrency(self) -> int:
|
||||
"""Resolve effective concurrency limit:
|
||||
0: per-device concurrency (multiple devices run concurrently, FIFO per device)
|
||||
1: global concurrency (strict single-task serialization across all devices)
|
||||
>1: global concurrency capped at N tasks across all devices
|
||||
"""
|
||||
if self.concurrency_mode is not None:
|
||||
mode = self.concurrency_mode
|
||||
if mode in ("global", "serial", "1"):
|
||||
return 1
|
||||
if mode in ("per_device", "device", "parallel", "0"):
|
||||
return 0
|
||||
if self.max_concurrency is not None:
|
||||
return self.max_concurrency
|
||||
"""All task entrypoints share the current single-task execution policy."""
|
||||
return self.resolve_env_concurrency()
|
||||
|
||||
@classmethod
|
||||
def resolve_env_concurrency(cls) -> int:
|
||||
"""Resolve the concurrency limit from the environment.
|
||||
"""Compatibility entrypoint shared with the scheduler: execution is serial.
|
||||
|
||||
Shared with the admin scheduler. Returns 0 for per-device concurrency,
|
||||
or a positive global limit.
|
||||
Legacy environment/SDK concurrency values cannot opt out in 2.2.0.
|
||||
The shared execution resource also makes admission atomic across processes.
|
||||
"""
|
||||
env_mode = os.environ.get("ARTEMIS_CONCURRENCY_MODE", "").strip().lower()
|
||||
if env_mode in ("global", "serial", "1"):
|
||||
return 1
|
||||
if env_mode in ("per_device", "device", "parallel", "0"):
|
||||
return 0
|
||||
try:
|
||||
return int(os.environ.get("ARTEMIS_MAX_CONCURRENT_TASKS", 0))
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
return 1
|
||||
|
||||
def acquire(
|
||||
self,
|
||||
@@ -768,6 +761,7 @@ class DeviceExecutionLock:
|
||||
|
||||
# 2. Determine FIFO eligibility for this specific device
|
||||
all_wait_files = sorted(self.queue_dir.glob("*.wait"))
|
||||
is_reentrant = False
|
||||
if effective_concurrency == 1:
|
||||
# In strict serial mode, global FIFO applies; re-entrant parent processes are always eligible
|
||||
active_owners = self.get_active_owners()
|
||||
@@ -794,7 +788,7 @@ class DeviceExecutionLock:
|
||||
)
|
||||
is_eligible = bool(device_queue and device_queue[0] == self._queue_path)
|
||||
|
||||
if is_eligible and self.resource_ids:
|
||||
if is_eligible and self.resource_ids and not is_reentrant:
|
||||
for earlier in all_wait_files:
|
||||
if earlier == self._queue_path:
|
||||
break
|
||||
|
||||
Reference in new issue
Block a user