cluxion-agentplugin-preprocessing 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/PKG-INFO +1 -1
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/pyproject.toml +1 -1
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/__init__.py +1 -1
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/dispatch_store.py +80 -48
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/resources/py_queue.py +87 -49
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_dispatch_store.py +72 -0
- cluxion_agentplugin_preprocessing-0.3.3/tests/runtime/test_py_queue_concurrency.py +160 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/.github/profile/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/.gitignore +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/Docs/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/LICENSE +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/adapters/claude/.claude-plugin/plugin.json +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/adapters/claude/skills/preprocess/SKILL.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/adapters/codex/config-snippet.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/cluxion-Docs/README.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/cluxion-Docs/architecture.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/cluxion-Docs/harness-logic.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/cluxion-Docs/honesty-preprocessing.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/cluxion-Docs/install-and-operations.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/cluxion-Docs/security.md +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/Cargo.lock +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/Cargo.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/pyproject.toml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/context.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/dispatch.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/guard.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/lib.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/main.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/queue.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/rust/cluxion_queue/src/types.rs +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/guard_watch.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/hermes_config.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/plugin.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/plugin.yaml +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/runner.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_agentplugin_preprocessing/schemas.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/__main__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/adapters/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/adapters/contract.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/adapters/grok_build.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/adapters/hermes.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/adapters/spec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/bootstrap.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/clarification.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/context_compress.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/harness.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/intent.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/ledger.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/ledger_codec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/plan_codec.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/preprocess.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/types.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/core/work_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/guard_daemon_host.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/models/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/models/supervisor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/models/vllm_mlx.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/resources/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/resources/guard_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/resources/queue_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/resources/rust_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/web/__init__.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/src/cluxion_runtime/web/browser_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_browser_bridge.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_clarification.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_cluxion_runtime_spine.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_context_compress.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_contract.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_guard.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_ledger.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_queue_backends.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_runtime_adapter_cli.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_rust_queue.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/runtime/test_supervisor.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/test_bootstrap.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/test_guard_watch.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/test_hermes_config.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/test_packaging_policy.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/test_plugin.py +0 -0
- {cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/tests/test_runner.py +0 -0
{cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cluxion-agentplugin-preprocessing
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff.
|
|
5
5
|
Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
|
|
6
6
|
Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-preprocessing
|
{cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cluxion-agentplugin-preprocessing"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Universal agent plugin for Cluxion preprocessing, honesty contracts, clarification, Rust work queue, and resource-aware harness handoff."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -5,11 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
7
|
import time
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from contextlib import contextmanager
|
|
8
10
|
from dataclasses import dataclass
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from tempfile import NamedTemporaryFile
|
|
11
13
|
from typing import TYPE_CHECKING
|
|
12
14
|
|
|
15
|
+
try:
|
|
16
|
+
import fcntl as _fcntl
|
|
17
|
+
except ImportError: # pragma: no cover - exercised only on non-POSIX platforms.
|
|
18
|
+
_fcntl = None
|
|
19
|
+
|
|
13
20
|
if TYPE_CHECKING:
|
|
14
21
|
from cluxion_runtime.core.types import HarnessPlan, QueueSegment
|
|
15
22
|
|
|
@@ -63,48 +70,43 @@ def persist_dispatch_bundle(plan: HarnessPlan, *, dispatch_dir: Path | None = No
|
|
|
63
70
|
pass
|
|
64
71
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
65
72
|
path = _bundle_path(plan.item.work_id, target_dir)
|
|
66
|
-
|
|
73
|
+
with _exclusive_bundle_lock(path):
|
|
74
|
+
_atomic_write_json(path, bundle)
|
|
67
75
|
return path
|
|
68
76
|
|
|
69
77
|
|
|
70
78
|
def load_dispatch_bundle(work_id: str, *, dispatch_dir: Path | None = None) -> dict[str, object]:
|
|
71
79
|
"""Read the dispatch bundle for a work_id."""
|
|
72
80
|
path = _bundle_path(work_id, default_dispatch_dir() if dispatch_dir is None else dispatch_dir)
|
|
73
|
-
|
|
74
|
-
raise DispatchStoreError(f"dispatch bundle not found: {work_id}")
|
|
75
|
-
try:
|
|
76
|
-
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
77
|
-
except json.JSONDecodeError as exc:
|
|
78
|
-
raise DispatchStoreError(f"dispatch bundle is invalid JSON: {work_id}") from exc
|
|
79
|
-
if not isinstance(payload, dict):
|
|
80
|
-
raise DispatchStoreError(f"dispatch bundle must be an object: {work_id}")
|
|
81
|
-
return payload
|
|
81
|
+
return _load_dispatch_bundle_from_path(path, work_id)
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
def next_dispatch_step(work_id: str, *, dispatch_dir: Path | None = None) -> dict[str, object]:
|
|
85
85
|
"""Mark the next queued segment as running and return the payload for Hermes."""
|
|
86
86
|
target_dir = default_dispatch_dir() if dispatch_dir is None else dispatch_dir
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
step
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
87
|
+
path = _bundle_path(work_id, target_dir)
|
|
88
|
+
with _exclusive_bundle_lock(path):
|
|
89
|
+
bundle = _load_dispatch_bundle_from_path(path, work_id)
|
|
90
|
+
steps = _steps(bundle)
|
|
91
|
+
for step in steps:
|
|
92
|
+
if step.get("status") in {"queued", "retry_wait"}:
|
|
93
|
+
step["status"] = "running"
|
|
94
|
+
step["updated_at"] = time.time()
|
|
95
|
+
_atomic_write_json(path, bundle)
|
|
96
|
+
return {
|
|
97
|
+
"work_id": work_id,
|
|
98
|
+
"ready": True,
|
|
99
|
+
"step": _public_step(step),
|
|
100
|
+
"remaining": _remaining_count(steps),
|
|
101
|
+
"synthesis_ready": False,
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
"work_id": work_id,
|
|
105
|
+
"ready": False,
|
|
106
|
+
"step": {},
|
|
107
|
+
"remaining": _remaining_count(steps),
|
|
108
|
+
"synthesis_ready": all(step.get("status") == "succeeded" for step in steps),
|
|
109
|
+
}
|
|
108
110
|
|
|
109
111
|
|
|
110
112
|
def record_dispatch_result(
|
|
@@ -118,23 +120,25 @@ def record_dispatch_result(
|
|
|
118
120
|
) -> dict[str, object]:
|
|
119
121
|
"""Store the segment result produced by the Hermes model."""
|
|
120
122
|
target_dir = default_dispatch_dir() if dispatch_dir is None else dispatch_dir
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
step
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
123
|
+
path = _bundle_path(work_id, target_dir)
|
|
124
|
+
with _exclusive_bundle_lock(path):
|
|
125
|
+
bundle = _load_dispatch_bundle_from_path(path, work_id)
|
|
126
|
+
steps = _steps(bundle)
|
|
127
|
+
for step in steps:
|
|
128
|
+
if step.get("step_id") == step_id:
|
|
129
|
+
step["status"] = "succeeded" if succeeded else "failed"
|
|
130
|
+
step["result"] = result
|
|
131
|
+
step["error"] = error
|
|
132
|
+
step["updated_at"] = time.time()
|
|
133
|
+
_atomic_write_json(path, bundle)
|
|
134
|
+
return {
|
|
135
|
+
"work_id": work_id,
|
|
136
|
+
"step_id": step_id,
|
|
137
|
+
"recorded": True,
|
|
138
|
+
"status": step["status"],
|
|
139
|
+
"remaining": _remaining_count(steps),
|
|
140
|
+
"synthesis_ready": all(item.get("status") == "succeeded" for item in steps),
|
|
141
|
+
}
|
|
138
142
|
raise DispatchStoreError(f"dispatch step not found: {work_id}/{step_id}")
|
|
139
143
|
|
|
140
144
|
|
|
@@ -246,6 +250,34 @@ def _bundle_path(work_id: str, dispatch_dir: Path) -> Path:
|
|
|
246
250
|
return dispatch_dir / f"{safe}.json"
|
|
247
251
|
|
|
248
252
|
|
|
253
|
+
def _load_dispatch_bundle_from_path(path: Path, work_id: str) -> dict[str, object]:
|
|
254
|
+
if not path.exists():
|
|
255
|
+
raise DispatchStoreError(f"dispatch bundle not found: {work_id}")
|
|
256
|
+
try:
|
|
257
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
258
|
+
except json.JSONDecodeError as exc:
|
|
259
|
+
raise DispatchStoreError(f"dispatch bundle is invalid JSON: {work_id}") from exc
|
|
260
|
+
if not isinstance(payload, dict):
|
|
261
|
+
raise DispatchStoreError(f"dispatch bundle must be an object: {work_id}")
|
|
262
|
+
return payload
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@contextmanager
|
|
266
|
+
def _exclusive_bundle_lock(path: Path) -> Iterator[None]:
|
|
267
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
if _fcntl is None:
|
|
269
|
+
# Non-POSIX platforms keep atomic rename but skip advisory locking.
|
|
270
|
+
yield
|
|
271
|
+
return
|
|
272
|
+
lock_path = path.with_name(f"{path.name}.lock")
|
|
273
|
+
with lock_path.open("a+b") as lock_file:
|
|
274
|
+
_fcntl.flock(lock_file.fileno(), _fcntl.LOCK_EX)
|
|
275
|
+
try:
|
|
276
|
+
yield
|
|
277
|
+
finally:
|
|
278
|
+
_fcntl.flock(lock_file.fileno(), _fcntl.LOCK_UN)
|
|
279
|
+
|
|
280
|
+
|
|
249
281
|
def _atomic_write_json(path: Path, payload: dict[str, object]) -> None:
|
|
250
282
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
251
283
|
with NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
|
@@ -12,9 +12,17 @@ import json
|
|
|
12
12
|
import os
|
|
13
13
|
import sqlite3
|
|
14
14
|
import time
|
|
15
|
+
from collections.abc import Iterator
|
|
16
|
+
from contextlib import contextmanager
|
|
15
17
|
from pathlib import Path
|
|
18
|
+
from tempfile import NamedTemporaryFile
|
|
16
19
|
from typing import Any
|
|
17
20
|
|
|
21
|
+
try:
|
|
22
|
+
import fcntl as _fcntl
|
|
23
|
+
except ImportError: # pragma: no cover - exercised only on non-POSIX platforms.
|
|
24
|
+
_fcntl = None
|
|
25
|
+
|
|
18
26
|
|
|
19
27
|
def run(command: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
20
28
|
store_dir = Path(payload.get("store_dir") or _default_store())
|
|
@@ -41,10 +49,11 @@ def _default_store() -> str:
|
|
|
41
49
|
|
|
42
50
|
def _open_db(store_dir: Path) -> sqlite3.Connection:
|
|
43
51
|
store_dir.mkdir(parents=True, exist_ok=True)
|
|
44
|
-
conn = sqlite3.connect(store_dir / "work_queue.sqlite")
|
|
52
|
+
conn = sqlite3.connect(store_dir / "work_queue.sqlite", timeout=30.0)
|
|
45
53
|
conn.executescript(
|
|
46
54
|
"""
|
|
47
55
|
PRAGMA journal_mode=WAL;
|
|
56
|
+
-- NORMAL+WAL can lose the latest commit on OS crash, but avoids corruption and keeps queue throughput balanced.
|
|
48
57
|
PRAGMA synchronous=NORMAL;
|
|
49
58
|
CREATE TABLE IF NOT EXISTS work_queue (
|
|
50
59
|
work_id TEXT PRIMARY KEY,
|
|
@@ -64,6 +73,10 @@ def _open_db(store_dir: Path) -> sqlite3.Connection:
|
|
|
64
73
|
return conn
|
|
65
74
|
|
|
66
75
|
|
|
76
|
+
def _begin_immediate(conn: sqlite3.Connection) -> None:
|
|
77
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
78
|
+
|
|
79
|
+
|
|
67
80
|
def _ok(data: dict[str, Any]) -> dict[str, Any]:
|
|
68
81
|
return {"ok": True, **data}
|
|
69
82
|
|
|
@@ -83,6 +96,7 @@ def _enqueue(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
83
96
|
metadata_json = json.dumps(payload.get("metadata", {}), ensure_ascii=False)
|
|
84
97
|
now = time.time()
|
|
85
98
|
with _open_db(store_dir) as conn:
|
|
99
|
+
_begin_immediate(conn)
|
|
86
100
|
sequence = conn.execute("SELECT COALESCE(MAX(sequence), 0) + 1 FROM work_queue").fetchone()[0]
|
|
87
101
|
conn.execute(
|
|
88
102
|
"""INSERT INTO work_queue (work_id, prompt, surface, priority, status, metadata_json, sequence, created_at, updated_at)
|
|
@@ -99,6 +113,7 @@ def _enqueue(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
99
113
|
def _dequeue(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
100
114
|
now = time.time()
|
|
101
115
|
with _open_db(store_dir) as conn:
|
|
116
|
+
_begin_immediate(conn)
|
|
102
117
|
row = conn.execute(
|
|
103
118
|
"""SELECT work_id, prompt, surface, priority, metadata_json FROM work_queue
|
|
104
119
|
WHERE status = 'pending' ORDER BY priority ASC, sequence ASC LIMIT 1"""
|
|
@@ -165,9 +180,13 @@ def _read_bundle(path: Path) -> dict[str, Any]:
|
|
|
165
180
|
|
|
166
181
|
def _write_atomic(path: Path, payload: dict[str, Any]) -> None:
|
|
167
182
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
183
|
+
with NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
|
184
|
+
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
185
|
+
handle.write("\n")
|
|
186
|
+
handle.flush()
|
|
187
|
+
os.fsync(handle.fileno())
|
|
188
|
+
temporary = Path(handle.name)
|
|
189
|
+
temporary.replace(path)
|
|
171
190
|
|
|
172
191
|
|
|
173
192
|
def _steps(bundle: dict[str, Any]) -> list[dict[str, Any]]:
|
|
@@ -186,7 +205,8 @@ def _persist(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
186
205
|
if "bundle" not in payload:
|
|
187
206
|
raise RuntimeError("missing bundle")
|
|
188
207
|
path = _bundle_path(store_dir, work_id)
|
|
189
|
-
|
|
208
|
+
with _exclusive_bundle_lock(path):
|
|
209
|
+
_write_atomic(path, payload["bundle"])
|
|
190
210
|
return _ok({"stored": True, "path": str(path)})
|
|
191
211
|
|
|
192
212
|
|
|
@@ -207,31 +227,32 @@ def _public_step(step: dict[str, Any]) -> dict[str, Any]:
|
|
|
207
227
|
def _next_step(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
208
228
|
work_id = _require_str(payload, "work_id")
|
|
209
229
|
path = _bundle_path(store_dir, work_id)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
step
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
with _exclusive_bundle_lock(path):
|
|
231
|
+
bundle = _read_bundle(path)
|
|
232
|
+
steps = _steps(bundle)
|
|
233
|
+
for step in steps:
|
|
234
|
+
if step.get("status") in ("queued", "retry_wait"):
|
|
235
|
+
step["status"] = "running"
|
|
236
|
+
step["updated_at"] = time.time()
|
|
237
|
+
_write_atomic(path, bundle)
|
|
238
|
+
return _ok(
|
|
239
|
+
{
|
|
240
|
+
"work_id": work_id,
|
|
241
|
+
"ready": True,
|
|
242
|
+
"step": _public_step(step),
|
|
243
|
+
"remaining": _remaining(steps),
|
|
244
|
+
"synthesis_ready": False,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
return _ok(
|
|
248
|
+
{
|
|
249
|
+
"work_id": work_id,
|
|
250
|
+
"ready": False,
|
|
251
|
+
"step": {},
|
|
252
|
+
"remaining": _remaining(steps),
|
|
253
|
+
"synthesis_ready": all(s.get("status") == "succeeded" for s in steps),
|
|
254
|
+
}
|
|
255
|
+
)
|
|
235
256
|
|
|
236
257
|
|
|
237
258
|
def _record_step(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -239,28 +260,45 @@ def _record_step(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
239
260
|
step_id = _require_str(payload, "step_id")
|
|
240
261
|
failed = bool(payload.get("failed", False))
|
|
241
262
|
path = _bundle_path(store_dir, work_id)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
step
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
263
|
+
with _exclusive_bundle_lock(path):
|
|
264
|
+
bundle = _read_bundle(path)
|
|
265
|
+
steps = _steps(bundle)
|
|
266
|
+
for step in steps:
|
|
267
|
+
if step.get("step_id") == step_id:
|
|
268
|
+
step["status"] = "failed" if failed else "succeeded"
|
|
269
|
+
step["result"] = payload.get("result", "")
|
|
270
|
+
step["error"] = payload.get("error", "")
|
|
271
|
+
step["updated_at"] = time.time()
|
|
272
|
+
_write_atomic(path, bundle)
|
|
273
|
+
return _ok(
|
|
274
|
+
{
|
|
275
|
+
"work_id": work_id,
|
|
276
|
+
"step_id": step_id,
|
|
277
|
+
"recorded": True,
|
|
278
|
+
"status": step["status"],
|
|
279
|
+
"remaining": _remaining(steps),
|
|
280
|
+
"synthesis_ready": all(s.get("status") == "succeeded" for s in steps),
|
|
281
|
+
}
|
|
282
|
+
)
|
|
261
283
|
raise RuntimeError(f"dispatch step not found: {work_id}/{step_id}")
|
|
262
284
|
|
|
263
285
|
|
|
286
|
+
@contextmanager
|
|
287
|
+
def _exclusive_bundle_lock(path: Path) -> Iterator[None]:
|
|
288
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
289
|
+
if _fcntl is None:
|
|
290
|
+
# Non-POSIX platforms keep atomic rename but skip advisory locking.
|
|
291
|
+
yield
|
|
292
|
+
return
|
|
293
|
+
lock_path = path.with_name(f"{path.name}.lock")
|
|
294
|
+
with lock_path.open("a+b") as lock_file:
|
|
295
|
+
_fcntl.flock(lock_file.fileno(), _fcntl.LOCK_EX)
|
|
296
|
+
try:
|
|
297
|
+
yield
|
|
298
|
+
finally:
|
|
299
|
+
_fcntl.flock(lock_file.fileno(), _fcntl.LOCK_UN)
|
|
300
|
+
|
|
301
|
+
|
|
264
302
|
def _brief(store_dir: Path, payload: dict[str, Any]) -> dict[str, Any]:
|
|
265
303
|
work_id = _require_str(payload, "work_id")
|
|
266
304
|
bundle = _read_bundle(_bundle_path(store_dir, work_id))
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
"""Concurrency coverage for the file-backed dispatch store."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
5
|
+
import threading
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from contextlib import suppress
|
|
3
8
|
from typing import TYPE_CHECKING
|
|
4
9
|
|
|
5
10
|
import pytest
|
|
6
11
|
|
|
12
|
+
from cluxion_runtime.core import dispatch_store
|
|
7
13
|
from cluxion_runtime.core.dispatch_store import (
|
|
8
14
|
DispatchStoreError,
|
|
9
15
|
build_briefing_payload,
|
|
@@ -21,6 +27,7 @@ if TYPE_CHECKING:
|
|
|
21
27
|
from cluxion_runtime.core.types import HarnessPlan
|
|
22
28
|
|
|
23
29
|
_SNAPSHOT = ResourceSnapshot(total_ram_mb=48_000, available_ram_mb=40_000, swap_used_mb=0, cpu_percent=20.0)
|
|
30
|
+
_RACE_TIMEOUT_SECONDS = 0.25
|
|
24
31
|
|
|
25
32
|
|
|
26
33
|
@pytest.fixture(scope="module")
|
|
@@ -51,6 +58,29 @@ def _drain_ids(work_id: str, dispatch_dir: Path) -> list[str]:
|
|
|
51
58
|
record_dispatch_result(work_id, step_id, result=f"done:{step_id}", dispatch_dir=dispatch_dir)
|
|
52
59
|
|
|
53
60
|
|
|
61
|
+
def _run_concurrently(worker_count: int, worker) -> list[object]:
|
|
62
|
+
start = threading.Barrier(worker_count)
|
|
63
|
+
|
|
64
|
+
def run(index: int) -> object:
|
|
65
|
+
start.wait()
|
|
66
|
+
return worker(index)
|
|
67
|
+
|
|
68
|
+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
|
69
|
+
return list(executor.map(run, range(worker_count)))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _stall_dispatch_writes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
73
|
+
original_write = dispatch_store._atomic_write_json
|
|
74
|
+
write_barrier = threading.Barrier(2)
|
|
75
|
+
|
|
76
|
+
def write(path: Path, payload: dict[str, object]) -> None:
|
|
77
|
+
with suppress(threading.BrokenBarrierError):
|
|
78
|
+
write_barrier.wait(timeout=_RACE_TIMEOUT_SECONDS)
|
|
79
|
+
original_write(path, payload)
|
|
80
|
+
|
|
81
|
+
monkeypatch.setattr(dispatch_store, "_atomic_write_json", write)
|
|
82
|
+
|
|
83
|
+
|
|
54
84
|
def test_persist_skips_plans_without_queue(tmp_path: Path) -> None:
|
|
55
85
|
plan = build_harness_plan(WorkItem("w-short", "작업: 작은 버그를 고쳐줘."), snapshot=_SNAPSHOT)
|
|
56
86
|
assert plan.execution.queue_required is False
|
|
@@ -78,6 +108,22 @@ def test_next_marks_step_running_on_disk(tmp_path: Path, queued_plan: HarnessPla
|
|
|
78
108
|
assert statuses.count("running") == 1
|
|
79
109
|
|
|
80
110
|
|
|
111
|
+
def test_concurrent_next_dispatch_steps_do_not_claim_same_step(
|
|
112
|
+
tmp_path: Path, queued_plan: HarnessPlan, monkeypatch: pytest.MonkeyPatch
|
|
113
|
+
) -> None:
|
|
114
|
+
"""Concurrent queue-next calls must not overwrite each other's running marker."""
|
|
115
|
+
persist_dispatch_bundle(queued_plan, dispatch_dir=tmp_path)
|
|
116
|
+
_stall_dispatch_writes(monkeypatch)
|
|
117
|
+
|
|
118
|
+
results = _run_concurrently(2, lambda _index: next_dispatch_step("w-queued", dispatch_dir=tmp_path))
|
|
119
|
+
step_ids = [str(result["step"]["step_id"]) for result in results if result["ready"]]
|
|
120
|
+
|
|
121
|
+
assert len(step_ids) == 2
|
|
122
|
+
assert len(set(step_ids)) == 2
|
|
123
|
+
statuses = [step["status"] for step in load_dispatch_bundle("w-queued", dispatch_dir=tmp_path)["steps"]]
|
|
124
|
+
assert statuses.count("running") == 2
|
|
125
|
+
|
|
126
|
+
|
|
81
127
|
def test_next_reports_not_ready_when_nothing_queued(tmp_path: Path, queued_plan: HarnessPlan) -> None:
|
|
82
128
|
persist_dispatch_bundle(queued_plan, dispatch_dir=tmp_path)
|
|
83
129
|
while next_dispatch_step("w-queued", dispatch_dir=tmp_path)["ready"]:
|
|
@@ -101,6 +147,32 @@ def test_full_drain_unlocks_briefing(tmp_path: Path, queued_plan: HarnessPlan) -
|
|
|
101
147
|
assert f"done:{step_id}" in str(briefing["briefing_prompt"])
|
|
102
148
|
|
|
103
149
|
|
|
150
|
+
def test_concurrent_record_dispatch_results_preserve_both_updates(
|
|
151
|
+
tmp_path: Path, queued_plan: HarnessPlan, monkeypatch: pytest.MonkeyPatch
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Concurrent queue-record calls for different steps must both persist."""
|
|
154
|
+
persist_dispatch_bundle(queued_plan, dispatch_dir=tmp_path)
|
|
155
|
+
step_ids = [
|
|
156
|
+
str(next_dispatch_step("w-queued", dispatch_dir=tmp_path)["step"]["step_id"]),
|
|
157
|
+
str(next_dispatch_step("w-queued", dispatch_dir=tmp_path)["step"]["step_id"]),
|
|
158
|
+
]
|
|
159
|
+
_stall_dispatch_writes(monkeypatch)
|
|
160
|
+
|
|
161
|
+
_run_concurrently(
|
|
162
|
+
2,
|
|
163
|
+
lambda index: record_dispatch_result(
|
|
164
|
+
"w-queued",
|
|
165
|
+
step_ids[index],
|
|
166
|
+
result=f"done:{index}",
|
|
167
|
+
dispatch_dir=tmp_path,
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
steps = {str(step["step_id"]): step for step in load_dispatch_bundle("w-queued", dispatch_dir=tmp_path)["steps"]}
|
|
172
|
+
assert steps[step_ids[0]]["result"] == "done:0"
|
|
173
|
+
assert steps[step_ids[1]]["result"] == "done:1"
|
|
174
|
+
|
|
175
|
+
|
|
104
176
|
def test_record_unknown_step_raises(tmp_path: Path, queued_plan: HarnessPlan) -> None:
|
|
105
177
|
persist_dispatch_bundle(queued_plan, dispatch_dir=tmp_path)
|
|
106
178
|
with pytest.raises(DispatchStoreError):
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Concurrency coverage for the pure-Python queue fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from cluxion_runtime.resources import py_queue
|
|
15
|
+
|
|
16
|
+
_RACE_TIMEOUT_SECONDS = 0.25
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run_concurrently(worker_count: int, worker) -> list[object]:
|
|
20
|
+
start = threading.Barrier(worker_count)
|
|
21
|
+
|
|
22
|
+
def run(index: int) -> object:
|
|
23
|
+
start.wait()
|
|
24
|
+
return worker(index)
|
|
25
|
+
|
|
26
|
+
with ThreadPoolExecutor(max_workers=worker_count) as executor:
|
|
27
|
+
return list(executor.map(run, range(worker_count)))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _bundle(step_count: int) -> dict[str, Any]:
|
|
31
|
+
return {
|
|
32
|
+
"work_id": "py-race",
|
|
33
|
+
"steps": [
|
|
34
|
+
{
|
|
35
|
+
"step_id": f"s{index}",
|
|
36
|
+
"segment_id": f"g{index}",
|
|
37
|
+
"checksum": f"c{index}",
|
|
38
|
+
"token_estimate": 10,
|
|
39
|
+
"content": f"segment {index}",
|
|
40
|
+
"status": "queued",
|
|
41
|
+
"result": "",
|
|
42
|
+
"error": "",
|
|
43
|
+
}
|
|
44
|
+
for index in range(step_count)
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _store_payload(store_dir: Path, **payload: Any) -> dict[str, Any]:
|
|
50
|
+
return {"store_dir": str(store_dir), **payload}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _dispatch_bundle_path(store_dir: Path, work_id: str) -> Path:
|
|
54
|
+
return store_dir / "dispatch" / f"{work_id}.json"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _read_dispatch_bundle(store_dir: Path, work_id: str) -> dict[str, Any]:
|
|
58
|
+
return json.loads(_dispatch_bundle_path(store_dir, work_id).read_text(encoding="utf-8"))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _stall_bundle_writes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
62
|
+
original_write = py_queue._write_atomic
|
|
63
|
+
write_barrier = threading.Barrier(2)
|
|
64
|
+
|
|
65
|
+
def write(path: Path, payload: dict[str, Any]) -> None:
|
|
66
|
+
with suppress(threading.BrokenBarrierError):
|
|
67
|
+
write_barrier.wait(timeout=_RACE_TIMEOUT_SECONDS)
|
|
68
|
+
original_write(path, payload)
|
|
69
|
+
|
|
70
|
+
monkeypatch.setattr(py_queue, "_write_atomic", write)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_python_queue_concurrent_next_steps_do_not_claim_same_step(
|
|
74
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Python fallback queue-next serializes JSON bundle updates."""
|
|
77
|
+
store_dir = tmp_path / "queue"
|
|
78
|
+
py_queue.run("persist", _store_payload(store_dir, work_id="py-race", bundle=_bundle(2)))
|
|
79
|
+
_stall_bundle_writes(monkeypatch)
|
|
80
|
+
|
|
81
|
+
results = _run_concurrently(2, lambda _index: py_queue.run("next", _store_payload(store_dir, work_id="py-race")))
|
|
82
|
+
step_ids = [str(result["step"]["step_id"]) for result in results if result["ready"]]
|
|
83
|
+
|
|
84
|
+
assert len(step_ids) == 2
|
|
85
|
+
assert len(set(step_ids)) == 2
|
|
86
|
+
statuses = [step["status"] for step in _read_dispatch_bundle(store_dir, "py-race")["steps"]]
|
|
87
|
+
assert statuses.count("running") == 2
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_python_queue_concurrent_record_steps_preserve_both_updates(
|
|
91
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Python fallback queue-record serializes JSON bundle updates."""
|
|
94
|
+
store_dir = tmp_path / "queue"
|
|
95
|
+
py_queue.run("persist", _store_payload(store_dir, work_id="py-race", bundle=_bundle(2)))
|
|
96
|
+
step_ids = [
|
|
97
|
+
str(py_queue.run("next", _store_payload(store_dir, work_id="py-race"))["step"]["step_id"]),
|
|
98
|
+
str(py_queue.run("next", _store_payload(store_dir, work_id="py-race"))["step"]["step_id"]),
|
|
99
|
+
]
|
|
100
|
+
_stall_bundle_writes(monkeypatch)
|
|
101
|
+
|
|
102
|
+
_run_concurrently(
|
|
103
|
+
2,
|
|
104
|
+
lambda index: py_queue.run(
|
|
105
|
+
"record",
|
|
106
|
+
_store_payload(
|
|
107
|
+
store_dir,
|
|
108
|
+
work_id="py-race",
|
|
109
|
+
step_id=step_ids[index],
|
|
110
|
+
result=f"done:{index}",
|
|
111
|
+
),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
steps = {step["step_id"]: step for step in _read_dispatch_bundle(store_dir, "py-race")["steps"]}
|
|
116
|
+
assert steps[step_ids[0]]["result"] == "done:0"
|
|
117
|
+
assert steps[step_ids[1]]["result"] == "done:1"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_python_queue_concurrent_dequeue_serializes_select_update(
|
|
121
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Python fallback dequeue uses a transaction so two workers cannot claim one row."""
|
|
124
|
+
store_dir = tmp_path / "queue"
|
|
125
|
+
for index in range(2):
|
|
126
|
+
py_queue.run(
|
|
127
|
+
"enqueue",
|
|
128
|
+
_store_payload(store_dir, work_id=f"w{index}", prompt=f"prompt {index}", priority=index),
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
original_open_db = py_queue._open_db
|
|
132
|
+
select_barrier = threading.Barrier(2)
|
|
133
|
+
|
|
134
|
+
class SlowConnection:
|
|
135
|
+
def __init__(self, conn) -> None:
|
|
136
|
+
self._conn = conn
|
|
137
|
+
|
|
138
|
+
def __enter__(self):
|
|
139
|
+
self._conn.__enter__()
|
|
140
|
+
return self
|
|
141
|
+
|
|
142
|
+
def __exit__(self, exc_type, exc, tb) -> bool | None:
|
|
143
|
+
return self._conn.__exit__(exc_type, exc, tb)
|
|
144
|
+
|
|
145
|
+
def execute(self, sql: str, parameters: tuple[object, ...] = ()):
|
|
146
|
+
if "SELECT work_id, prompt, surface, priority, metadata_json FROM work_queue" in sql:
|
|
147
|
+
with suppress(threading.BrokenBarrierError):
|
|
148
|
+
select_barrier.wait(timeout=_RACE_TIMEOUT_SECONDS)
|
|
149
|
+
return self._conn.execute(sql, parameters)
|
|
150
|
+
|
|
151
|
+
def open_db(path: Path) -> SlowConnection:
|
|
152
|
+
return SlowConnection(original_open_db(path))
|
|
153
|
+
|
|
154
|
+
monkeypatch.setattr(py_queue, "_open_db", open_db)
|
|
155
|
+
|
|
156
|
+
results = _run_concurrently(2, lambda _index: py_queue.run("dequeue", _store_payload(store_dir)))
|
|
157
|
+
work_ids = [str(result["item"]["work_id"]) for result in results if result["ready"]]
|
|
158
|
+
|
|
159
|
+
assert len(work_ids) == 2
|
|
160
|
+
assert len(set(work_ids)) == 2
|
|
File without changes
|
{cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/.gitignore
RENAMED
|
File without changes
|
{cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/Docs/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{cluxion_agentplugin_preprocessing-0.3.2 → cluxion_agentplugin_preprocessing-0.3.3}/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|