crackerjack 0.33.0__py3-none-any.whl → 0.33.2__py3-none-any.whl
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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +4 -13
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +104 -204
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +171 -174
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +17 -16
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +6 -35
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +15 -9
- crackerjack/services/config_merge.py +19 -80
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +27 -25
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +618 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
- crackerjack-0.33.2.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
"""Global hook lock management to prevent concurrent execution of specific hooks.
|
|
2
|
-
|
|
3
|
-
This module provides a lock manager that ensures certain hooks
|
|
4
|
-
(like complexipy) run sequentially rather than concurrently to prevent
|
|
5
|
-
resource contention and hanging processes.
|
|
6
|
-
|
|
7
|
-
This implements the HookLockManagerProtocol for dependency injection compatibility.
|
|
8
|
-
|
|
9
|
-
Phase 2 implementation provides enhanced file-based global lock coordination
|
|
10
|
-
across multiple crackerjack sessions with atomic operations, heartbeat monitoring,
|
|
11
|
-
and comprehensive stale lock cleanup.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
1
|
import asyncio
|
|
15
2
|
import json
|
|
16
3
|
import logging
|
|
@@ -18,19 +5,13 @@ import os
|
|
|
18
5
|
import time
|
|
19
6
|
import typing as t
|
|
20
7
|
from collections import defaultdict
|
|
21
|
-
from contextlib import asynccontextmanager, suppress
|
|
8
|
+
from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress
|
|
22
9
|
from pathlib import Path
|
|
23
10
|
|
|
24
11
|
from ..config.global_lock_config import GlobalLockConfig
|
|
25
12
|
|
|
26
13
|
|
|
27
14
|
class HookLockManager:
|
|
28
|
-
"""Manager for hook-specific locks to prevent concurrent execution.
|
|
29
|
-
|
|
30
|
-
Implements HookLockManagerProtocol for dependency injection compatibility.
|
|
31
|
-
Provides async locking with timeout protection and comprehensive monitoring.
|
|
32
|
-
"""
|
|
33
|
-
|
|
34
15
|
_instance: t.Optional["HookLockManager"] = None
|
|
35
16
|
_initialized: bool = False
|
|
36
17
|
|
|
@@ -44,37 +25,30 @@ class HookLockManager:
|
|
|
44
25
|
return
|
|
45
26
|
|
|
46
27
|
self._hooks_requiring_locks = {
|
|
47
|
-
"complexipy",
|
|
48
|
-
# Add other hooks that should run sequentially
|
|
28
|
+
"complexipy",
|
|
49
29
|
}
|
|
50
30
|
|
|
51
|
-
# Per-hook locks for sequential execution
|
|
52
31
|
self._hook_locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
|
53
32
|
|
|
54
|
-
# Global lock configuration and state
|
|
55
33
|
self._global_config = GlobalLockConfig()
|
|
56
34
|
self._global_lock_enabled = self._global_config.enabled
|
|
57
35
|
self._active_global_locks: set[str] = set()
|
|
58
36
|
self._heartbeat_tasks: dict[str, asyncio.Task[None]] = {}
|
|
59
37
|
|
|
60
|
-
# Lock usage tracking for monitoring
|
|
61
38
|
self._lock_usage: dict[str, list[float]] = defaultdict(list)
|
|
62
39
|
self._lock_wait_times: dict[str, list[float]] = defaultdict(list)
|
|
63
40
|
self._lock_execution_times: dict[str, list[float]] = defaultdict(list)
|
|
64
|
-
self._max_history = 50
|
|
41
|
+
self._max_history = 50
|
|
65
42
|
|
|
66
|
-
# Global lock statistics tracking
|
|
67
43
|
self._global_lock_attempts: dict[str, int] = defaultdict(int)
|
|
68
44
|
self._global_lock_successes: dict[str, int] = defaultdict(int)
|
|
69
45
|
self._global_lock_failures: dict[str, int] = defaultdict(int)
|
|
70
46
|
self._stale_locks_cleaned: dict[str, int] = defaultdict(int)
|
|
71
47
|
self._heartbeat_failures: dict[str, int] = defaultdict(int)
|
|
72
48
|
|
|
73
|
-
|
|
74
|
-
self._default_lock_timeout = 300.0 # 5 minutes default timeout
|
|
49
|
+
self._default_lock_timeout = 300.0
|
|
75
50
|
self._lock_timeouts: dict[str, float] = {}
|
|
76
51
|
|
|
77
|
-
# Error tracking
|
|
78
52
|
self._lock_failures: dict[str, int] = defaultdict(int)
|
|
79
53
|
self._timeout_failures: dict[str, int] = defaultdict(int)
|
|
80
54
|
|
|
@@ -82,41 +56,22 @@ class HookLockManager:
|
|
|
82
56
|
self._initialized = True
|
|
83
57
|
|
|
84
58
|
def requires_lock(self, hook_name: str) -> bool:
|
|
85
|
-
"""Check if a hook requires sequential execution."""
|
|
86
59
|
return hook_name in self._hooks_requiring_locks
|
|
87
60
|
|
|
88
61
|
@asynccontextmanager
|
|
89
|
-
async def acquire_hook_lock(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
hook_name: Name of the hook to lock
|
|
94
|
-
|
|
95
|
-
Yields:
|
|
96
|
-
None when lock is acquired (or hook doesn't require lock)
|
|
97
|
-
|
|
98
|
-
Raises:
|
|
99
|
-
asyncio.TimeoutError: If lock acquisition times out
|
|
100
|
-
|
|
101
|
-
Example:
|
|
102
|
-
async with lock_manager.acquire_hook_lock("complexipy"):
|
|
103
|
-
# Only one complexipy process will run at a time
|
|
104
|
-
result = await execute_hook(hook)
|
|
105
|
-
"""
|
|
62
|
+
async def acquire_hook_lock(
|
|
63
|
+
self, hook_name: str
|
|
64
|
+
) -> AbstractAsyncContextManager[None]:
|
|
106
65
|
if not self.requires_lock(hook_name):
|
|
107
|
-
# Hook doesn't require locking, proceed immediately
|
|
108
66
|
yield
|
|
109
67
|
return
|
|
110
68
|
|
|
111
69
|
if not self._global_lock_enabled:
|
|
112
|
-
# Use existing hook-specific locking only (legacy behavior)
|
|
113
70
|
async with self._acquire_existing_hook_lock(hook_name):
|
|
114
71
|
yield
|
|
115
72
|
return
|
|
116
73
|
|
|
117
|
-
# Global locking: coordinate across all crackerjack sessions
|
|
118
74
|
async with self._acquire_global_coordination_lock(hook_name):
|
|
119
|
-
# Then acquire hook-specific lock within global coordination
|
|
120
75
|
async with self._acquire_existing_hook_lock(hook_name):
|
|
121
76
|
yield
|
|
122
77
|
|
|
@@ -124,7 +79,6 @@ class HookLockManager:
|
|
|
124
79
|
async def _acquire_existing_hook_lock(
|
|
125
80
|
self, hook_name: str
|
|
126
81
|
) -> t.AsyncIterator[None]:
|
|
127
|
-
"""Acquire hook-specific asyncio lock (original behavior)."""
|
|
128
82
|
lock = self._hook_locks[hook_name]
|
|
129
83
|
timeout = self._lock_timeouts.get(hook_name, self._default_lock_timeout)
|
|
130
84
|
start_time = time.time()
|
|
@@ -134,17 +88,15 @@ class HookLockManager:
|
|
|
134
88
|
)
|
|
135
89
|
|
|
136
90
|
try:
|
|
137
|
-
# Use asyncio.wait_for to implement timeout for lock acquisition
|
|
138
91
|
await asyncio.wait_for(lock.acquire(), timeout=timeout)
|
|
139
92
|
|
|
140
93
|
try:
|
|
141
94
|
acquisition_time = time.time() - start_time
|
|
142
95
|
self.logger.info(
|
|
143
96
|
f"Hook-specific lock acquired for {hook_name} after"
|
|
144
|
-
f" {acquisition_time
|
|
97
|
+
f" {acquisition_time: .2f}s"
|
|
145
98
|
)
|
|
146
99
|
|
|
147
|
-
# Track lock usage for monitoring
|
|
148
100
|
self._track_lock_usage(hook_name, acquisition_time)
|
|
149
101
|
|
|
150
102
|
execution_start = time.time()
|
|
@@ -157,11 +109,10 @@ class HookLockManager:
|
|
|
157
109
|
self._track_lock_execution(hook_name, execution_time, total_time)
|
|
158
110
|
self.logger.debug(
|
|
159
111
|
f"Hook-specific lock released for {hook_name} after"
|
|
160
|
-
f" {total_time
|
|
112
|
+
f" {total_time: .2f}s total"
|
|
161
113
|
)
|
|
162
114
|
|
|
163
115
|
finally:
|
|
164
|
-
# Always release the lock, even if an exception occurred
|
|
165
116
|
lock.release()
|
|
166
117
|
|
|
167
118
|
except TimeoutError:
|
|
@@ -169,7 +120,7 @@ class HookLockManager:
|
|
|
169
120
|
wait_time = time.time() - start_time
|
|
170
121
|
self.logger.error(
|
|
171
122
|
f"Hook-specific lock acquisition timeout for {hook_name} after"
|
|
172
|
-
f" {wait_time
|
|
123
|
+
f" {wait_time: .2f}s "
|
|
173
124
|
f"(timeout: {timeout}s, total failures: "
|
|
174
125
|
f"{self._timeout_failures[hook_name]})"
|
|
175
126
|
)
|
|
@@ -187,7 +138,6 @@ class HookLockManager:
|
|
|
187
138
|
async def _acquire_global_coordination_lock(
|
|
188
139
|
self, hook_name: str
|
|
189
140
|
) -> t.AsyncIterator[None]:
|
|
190
|
-
"""Acquire global file-based coordination lock across crackerjack sessions."""
|
|
191
141
|
lock_path = self._global_config.get_lock_path(hook_name)
|
|
192
142
|
start_time = time.time()
|
|
193
143
|
|
|
@@ -196,28 +146,24 @@ class HookLockManager:
|
|
|
196
146
|
f"Attempting global lock acquisition for {hook_name}: {lock_path}"
|
|
197
147
|
)
|
|
198
148
|
|
|
199
|
-
# Clean up stale locks first
|
|
200
149
|
await self._cleanup_stale_lock_if_needed(hook_name)
|
|
201
150
|
|
|
202
151
|
try:
|
|
203
|
-
# Atomic lock acquisition with retry logic
|
|
204
152
|
await self._acquire_global_lock_file(hook_name, lock_path)
|
|
205
153
|
self._global_lock_successes[hook_name] += 1
|
|
206
154
|
self._active_global_locks.add(hook_name)
|
|
207
155
|
|
|
208
|
-
# Start heartbeat to keep lock alive
|
|
209
156
|
heartbeat_task = asyncio.create_task(self._maintain_heartbeat(hook_name))
|
|
210
157
|
self._heartbeat_tasks[hook_name] = heartbeat_task
|
|
211
158
|
|
|
212
159
|
acquisition_time = time.time() - start_time
|
|
213
160
|
self.logger.info(
|
|
214
|
-
f"Global lock acquired for {hook_name} after {acquisition_time
|
|
161
|
+
f"Global lock acquired for {hook_name} after {acquisition_time: .2f}s"
|
|
215
162
|
)
|
|
216
163
|
|
|
217
164
|
try:
|
|
218
165
|
yield
|
|
219
166
|
finally:
|
|
220
|
-
# Cleanup: cancel heartbeat and remove lock file
|
|
221
167
|
await self._cleanup_global_lock(hook_name, heartbeat_task)
|
|
222
168
|
|
|
223
169
|
except Exception as e:
|
|
@@ -226,14 +172,12 @@ class HookLockManager:
|
|
|
226
172
|
raise
|
|
227
173
|
|
|
228
174
|
def _track_lock_usage(self, hook_name: str, acquisition_time: float) -> None:
|
|
229
|
-
"""Track lock acquisition times for monitoring."""
|
|
230
175
|
usage_list = self._lock_usage[hook_name]
|
|
231
176
|
wait_list = self._lock_wait_times[hook_name]
|
|
232
177
|
|
|
233
178
|
usage_list.append(acquisition_time)
|
|
234
179
|
wait_list.append(acquisition_time)
|
|
235
180
|
|
|
236
|
-
# Keep only recent history
|
|
237
181
|
if len(usage_list) > self._max_history:
|
|
238
182
|
usage_list.pop(0)
|
|
239
183
|
if len(wait_list) > self._max_history:
|
|
@@ -242,35 +186,31 @@ class HookLockManager:
|
|
|
242
186
|
def _track_lock_execution(
|
|
243
187
|
self, hook_name: str, execution_time: float, total_time: float
|
|
244
188
|
) -> None:
|
|
245
|
-
"""Track lock execution times for monitoring."""
|
|
246
189
|
exec_list = self._lock_execution_times[hook_name]
|
|
247
190
|
exec_list.append(execution_time)
|
|
248
191
|
|
|
249
|
-
# Keep only recent history
|
|
250
192
|
if len(exec_list) > self._max_history:
|
|
251
193
|
exec_list.pop(0)
|
|
252
194
|
|
|
253
195
|
self.logger.debug(
|
|
254
|
-
f"Hook {hook_name} execution: {execution_time
|
|
255
|
-
f"(total with lock: {total_time
|
|
196
|
+
f"Hook {hook_name} execution: {execution_time: .2f}s "
|
|
197
|
+
f"(total with lock: {total_time: .2f}s)"
|
|
256
198
|
)
|
|
257
199
|
|
|
258
200
|
async def _acquire_global_lock_file(self, hook_name: str, lock_path: Path) -> None:
|
|
259
|
-
"""Atomic acquisition of global lock file with retry logic."""
|
|
260
201
|
for attempt in range(self._global_config.max_retry_attempts):
|
|
261
202
|
try:
|
|
262
203
|
await self._attempt_lock_acquisition(hook_name, lock_path)
|
|
263
204
|
return
|
|
264
205
|
except FileExistsError:
|
|
265
206
|
if attempt < self._global_config.max_retry_attempts - 1:
|
|
266
|
-
# Exponential backoff with jitter
|
|
267
207
|
delay = self._global_config.retry_delay_seconds * (2**attempt)
|
|
268
|
-
jitter = delay * 0.1
|
|
208
|
+
jitter = delay * 0.1
|
|
269
209
|
wait_time = delay + (jitter * (0.5 - os.urandom(1)[0] / 255))
|
|
270
210
|
|
|
271
211
|
self.logger.debug(
|
|
272
212
|
f"Global lock exists for {hook_name}, retrying in "
|
|
273
|
-
f"{wait_time
|
|
213
|
+
f"{wait_time: .2f}s"
|
|
274
214
|
)
|
|
275
215
|
await asyncio.sleep(wait_time)
|
|
276
216
|
else:
|
|
@@ -280,7 +220,6 @@ class HookLockManager:
|
|
|
280
220
|
)
|
|
281
221
|
|
|
282
222
|
async def _attempt_lock_acquisition(self, hook_name: str, lock_path: Path) -> None:
|
|
283
|
-
"""Single atomic lock acquisition attempt using temp file + rename pattern."""
|
|
284
223
|
temp_path = lock_path.with_suffix(".tmp")
|
|
285
224
|
|
|
286
225
|
lock_data = {
|
|
@@ -290,39 +229,32 @@ class HookLockManager:
|
|
|
290
229
|
"hook_name": hook_name,
|
|
291
230
|
"acquired_at": time.time(),
|
|
292
231
|
"last_heartbeat": time.time(),
|
|
293
|
-
"crackerjack_version": "0.30.3",
|
|
232
|
+
"crackerjack_version": "0.30.3",
|
|
294
233
|
}
|
|
295
234
|
|
|
296
235
|
try:
|
|
297
|
-
# Use exclusive creation for atomic operation
|
|
298
236
|
with temp_path.open("x", encoding="utf-8") as f:
|
|
299
237
|
json.dump(lock_data, f, indent=2)
|
|
300
238
|
|
|
301
|
-
# Set restrictive permissions (owner only)
|
|
302
239
|
temp_path.chmod(0o600)
|
|
303
240
|
|
|
304
|
-
# Atomic rename - this is the critical section
|
|
305
241
|
try:
|
|
306
242
|
temp_path.rename(lock_path)
|
|
307
243
|
self.logger.debug(f"Successfully created global lock file: {lock_path}")
|
|
308
244
|
except FileExistsError:
|
|
309
|
-
# Another process won the race, clean up our temp file
|
|
310
245
|
with suppress(OSError):
|
|
311
246
|
temp_path.unlink()
|
|
312
247
|
raise
|
|
313
248
|
|
|
314
249
|
except FileExistsError:
|
|
315
|
-
# Lock file already exists - convert to proper exception type
|
|
316
250
|
raise FileExistsError(f"Global lock already exists for {hook_name}")
|
|
317
251
|
except Exception as e:
|
|
318
|
-
# Clean up temp file on any error
|
|
319
252
|
with suppress(OSError):
|
|
320
253
|
temp_path.unlink()
|
|
321
254
|
self.logger.error(f"Failed to create global lock for {hook_name}: {e}")
|
|
322
255
|
raise
|
|
323
256
|
|
|
324
257
|
async def _maintain_heartbeat(self, hook_name: str) -> None:
|
|
325
|
-
"""Maintain heartbeat updates to prevent stale lock detection."""
|
|
326
258
|
lock_path = self._global_config.get_lock_path(hook_name)
|
|
327
259
|
interval = self._global_config.session_heartbeat_interval
|
|
328
260
|
|
|
@@ -335,7 +267,6 @@ class HookLockManager:
|
|
|
335
267
|
if hook_name not in self._active_global_locks:
|
|
336
268
|
break
|
|
337
269
|
|
|
338
|
-
# Update heartbeat timestamp in lock file
|
|
339
270
|
await self._update_heartbeat_timestamp(hook_name, lock_path)
|
|
340
271
|
|
|
341
272
|
except asyncio.CancelledError:
|
|
@@ -345,10 +276,9 @@ class HookLockManager:
|
|
|
345
276
|
self._heartbeat_failures[hook_name] += 1
|
|
346
277
|
self.logger.warning(f"Heartbeat update failed for {hook_name}: {e}")
|
|
347
278
|
|
|
348
|
-
# If too many heartbeat failures, consider the lock compromised
|
|
349
279
|
if self._heartbeat_failures[hook_name] > 3:
|
|
350
280
|
self.logger.error(
|
|
351
|
-
f"Too many heartbeat failures for {hook_name},"
|
|
281
|
+
f"Too many heartbeat failures for {hook_name}, "
|
|
352
282
|
f" stopping heartbeat"
|
|
353
283
|
)
|
|
354
284
|
break
|
|
@@ -356,7 +286,6 @@ class HookLockManager:
|
|
|
356
286
|
async def _update_heartbeat_timestamp(
|
|
357
287
|
self, hook_name: str, lock_path: Path
|
|
358
288
|
) -> None:
|
|
359
|
-
"""Atomic update of heartbeat timestamp in existing lock file."""
|
|
360
289
|
if not lock_path.exists():
|
|
361
290
|
self.logger.warning(
|
|
362
291
|
f"Lock file disappeared for {hook_name}, stopping heartbeat"
|
|
@@ -367,11 +296,9 @@ class HookLockManager:
|
|
|
367
296
|
temp_path = lock_path.with_suffix(".heartbeat_tmp")
|
|
368
297
|
|
|
369
298
|
try:
|
|
370
|
-
# Read existing lock data
|
|
371
299
|
with lock_path.open(encoding="utf-8") as f:
|
|
372
300
|
lock_data = json.load(f)
|
|
373
301
|
|
|
374
|
-
# Verify we still own this lock
|
|
375
302
|
if lock_data.get("session_id") != self._global_config.session_id:
|
|
376
303
|
self.logger.warning(
|
|
377
304
|
f"Lock ownership changed for {hook_name}, stopping heartbeat"
|
|
@@ -379,10 +306,8 @@ class HookLockManager:
|
|
|
379
306
|
self._active_global_locks.discard(hook_name)
|
|
380
307
|
return
|
|
381
308
|
|
|
382
|
-
# Update heartbeat timestamp
|
|
383
309
|
lock_data["last_heartbeat"] = time.time()
|
|
384
310
|
|
|
385
|
-
# Write updated data atomically
|
|
386
311
|
with temp_path.open("w", encoding="utf-8") as f:
|
|
387
312
|
json.dump(lock_data, f, indent=2)
|
|
388
313
|
|
|
@@ -397,17 +322,13 @@ class HookLockManager:
|
|
|
397
322
|
async def _cleanup_global_lock(
|
|
398
323
|
self, hook_name: str, heartbeat_task: asyncio.Task[None] | None = None
|
|
399
324
|
) -> None:
|
|
400
|
-
"""Clean up global lock resources and remove lock file."""
|
|
401
325
|
self.logger.debug(f"Cleaning up global lock for {hook_name}")
|
|
402
326
|
|
|
403
|
-
# Stop tracking this lock
|
|
404
327
|
self._active_global_locks.discard(hook_name)
|
|
405
328
|
|
|
406
|
-
# Cancel heartbeat task
|
|
407
329
|
if heartbeat_task is None:
|
|
408
330
|
heartbeat_task = self._heartbeat_tasks.pop(hook_name, None)
|
|
409
331
|
else:
|
|
410
|
-
# Remove from task tracking
|
|
411
332
|
self._heartbeat_tasks.pop(hook_name, None)
|
|
412
333
|
|
|
413
334
|
if heartbeat_task:
|
|
@@ -415,11 +336,9 @@ class HookLockManager:
|
|
|
415
336
|
with suppress(asyncio.CancelledError):
|
|
416
337
|
await heartbeat_task
|
|
417
338
|
|
|
418
|
-
# Remove lock file
|
|
419
339
|
lock_path = self._global_config.get_lock_path(hook_name)
|
|
420
340
|
with suppress(OSError):
|
|
421
341
|
if lock_path.exists():
|
|
422
|
-
# Verify we still own the lock before deleting
|
|
423
342
|
try:
|
|
424
343
|
with lock_path.open(encoding="utf-8") as f:
|
|
425
344
|
lock_data = json.load(f)
|
|
@@ -438,14 +357,12 @@ class HookLockManager:
|
|
|
438
357
|
)
|
|
439
358
|
|
|
440
359
|
async def _cleanup_stale_lock_if_needed(self, hook_name: str) -> None:
|
|
441
|
-
"""Check for and remove stale lock if detected."""
|
|
442
360
|
lock_path = self._global_config.get_lock_path(hook_name)
|
|
443
361
|
|
|
444
362
|
if not lock_path.exists():
|
|
445
363
|
return
|
|
446
364
|
|
|
447
365
|
try:
|
|
448
|
-
# Check if lock is stale
|
|
449
366
|
with lock_path.open(encoding="utf-8") as f:
|
|
450
367
|
lock_data = json.load(f)
|
|
451
368
|
|
|
@@ -456,12 +373,11 @@ class HookLockManager:
|
|
|
456
373
|
|
|
457
374
|
if age_hours > self._global_config.stale_lock_hours:
|
|
458
375
|
self.logger.warning(
|
|
459
|
-
f"Removing stale lock for {hook_name} (age: {age_hours
|
|
376
|
+
f"Removing stale lock for {hook_name} (age: {age_hours: .2f}h)"
|
|
460
377
|
)
|
|
461
378
|
lock_path.unlink()
|
|
462
379
|
self._stale_locks_cleaned[hook_name] += 1
|
|
463
380
|
else:
|
|
464
|
-
# Lock is not stale, someone else has it
|
|
465
381
|
owner = lock_data.get("session_id", "unknown")
|
|
466
382
|
self.logger.debug(
|
|
467
383
|
f"Active lock exists for {hook_name} owned by {owner}"
|
|
@@ -469,13 +385,12 @@ class HookLockManager:
|
|
|
469
385
|
|
|
470
386
|
except Exception as e:
|
|
471
387
|
self.logger.warning(f"Could not check lock staleness for {hook_name}: {e}")
|
|
472
|
-
|
|
388
|
+
|
|
473
389
|
with suppress(OSError):
|
|
474
390
|
lock_path.unlink()
|
|
475
391
|
self._stale_locks_cleaned[hook_name] += 1
|
|
476
392
|
|
|
477
393
|
def get_lock_stats(self) -> dict[str, t.Any]:
|
|
478
|
-
"""Get comprehensive statistics about lock usage for monitoring."""
|
|
479
394
|
stats = {}
|
|
480
395
|
|
|
481
396
|
for hook_name in self._hooks_requiring_locks:
|
|
@@ -518,7 +433,6 @@ class HookLockManager:
|
|
|
518
433
|
),
|
|
519
434
|
}
|
|
520
435
|
|
|
521
|
-
# Wait time statistics
|
|
522
436
|
if wait_times:
|
|
523
437
|
base_stats.update(
|
|
524
438
|
{
|
|
@@ -536,7 +450,6 @@ class HookLockManager:
|
|
|
536
450
|
}
|
|
537
451
|
)
|
|
538
452
|
|
|
539
|
-
# Execution time statistics
|
|
540
453
|
if exec_times:
|
|
541
454
|
base_stats.update(
|
|
542
455
|
{
|
|
@@ -559,12 +472,10 @@ class HookLockManager:
|
|
|
559
472
|
return stats
|
|
560
473
|
|
|
561
474
|
def add_hook_to_lock_list(self, hook_name: str) -> None:
|
|
562
|
-
"""Add a hook to the list requiring sequential execution."""
|
|
563
475
|
self._hooks_requiring_locks.add(hook_name)
|
|
564
476
|
self.logger.info(f"Added {hook_name} to hooks requiring locks")
|
|
565
477
|
|
|
566
478
|
def remove_hook_from_lock_list(self, hook_name: str) -> None:
|
|
567
|
-
"""Remove a hook from the list requiring sequential execution."""
|
|
568
479
|
self._hooks_requiring_locks.discard(hook_name)
|
|
569
480
|
if hook_name in self._hook_locks:
|
|
570
481
|
del self._hook_locks[hook_name]
|
|
@@ -573,40 +484,18 @@ class HookLockManager:
|
|
|
573
484
|
self.logger.info(f"Removed {hook_name} from hooks requiring locks")
|
|
574
485
|
|
|
575
486
|
def is_hook_currently_locked(self, hook_name: str) -> bool:
|
|
576
|
-
"""Check if a hook is currently locked."""
|
|
577
487
|
if not self.requires_lock(hook_name):
|
|
578
488
|
return False
|
|
579
489
|
return self._hook_locks[hook_name].locked()
|
|
580
490
|
|
|
581
491
|
def set_hook_timeout(self, hook_name: str, timeout: float) -> None:
|
|
582
|
-
"""Set custom timeout for a specific hook.
|
|
583
|
-
|
|
584
|
-
Args:
|
|
585
|
-
hook_name: Name of the hook
|
|
586
|
-
timeout: Timeout in seconds
|
|
587
|
-
"""
|
|
588
492
|
self._lock_timeouts[hook_name] = timeout
|
|
589
493
|
self.logger.info(f"Set custom timeout for {hook_name}: {timeout}s")
|
|
590
494
|
|
|
591
495
|
def get_hook_timeout(self, hook_name: str) -> float:
|
|
592
|
-
"""Get timeout for a specific hook.
|
|
593
|
-
|
|
594
|
-
Args:
|
|
595
|
-
hook_name: Name of the hook
|
|
596
|
-
|
|
597
|
-
Returns:
|
|
598
|
-
Timeout in seconds
|
|
599
|
-
"""
|
|
600
496
|
return self._lock_timeouts.get(hook_name, self._default_lock_timeout)
|
|
601
497
|
|
|
602
|
-
# New protocol methods for global lock functionality
|
|
603
|
-
|
|
604
498
|
def enable_global_lock(self, enabled: bool = True) -> None:
|
|
605
|
-
"""Enable or disable global lock functionality.
|
|
606
|
-
|
|
607
|
-
Args:
|
|
608
|
-
enabled: Whether to enable global locking
|
|
609
|
-
"""
|
|
610
499
|
self._global_lock_enabled = enabled
|
|
611
500
|
self._global_config.enabled = enabled
|
|
612
501
|
self.logger.info(
|
|
@@ -614,33 +503,12 @@ class HookLockManager:
|
|
|
614
503
|
)
|
|
615
504
|
|
|
616
505
|
def is_global_lock_enabled(self) -> bool:
|
|
617
|
-
"""Check if global lock functionality is enabled.
|
|
618
|
-
|
|
619
|
-
Returns:
|
|
620
|
-
True if global locking is enabled
|
|
621
|
-
"""
|
|
622
506
|
return self._global_lock_enabled
|
|
623
507
|
|
|
624
508
|
def get_global_lock_path(self, hook_name: str) -> Path:
|
|
625
|
-
"""Get the filesystem path for a hook's global lock file.
|
|
626
|
-
|
|
627
|
-
Args:
|
|
628
|
-
hook_name: Name of the hook
|
|
629
|
-
|
|
630
|
-
Returns:
|
|
631
|
-
Path to the lock file for the hook
|
|
632
|
-
"""
|
|
633
509
|
return self._global_config.get_lock_path(hook_name)
|
|
634
510
|
|
|
635
511
|
def cleanup_stale_locks(self, max_age_hours: float = 2.0) -> int:
|
|
636
|
-
"""Clean up stale lock files older than max_age_hours.
|
|
637
|
-
|
|
638
|
-
Args:
|
|
639
|
-
max_age_hours: Maximum age in hours before a lock is considered stale
|
|
640
|
-
|
|
641
|
-
Returns:
|
|
642
|
-
Number of stale locks cleaned up
|
|
643
|
-
"""
|
|
644
512
|
locks_dir = self._global_config.lock_directory
|
|
645
513
|
if not locks_dir.exists():
|
|
646
514
|
return 0
|
|
@@ -665,9 +533,7 @@ class HookLockManager:
|
|
|
665
533
|
def _process_lock_file(
|
|
666
534
|
self, lock_file: Path, max_age_hours: float, current_time: float
|
|
667
535
|
) -> int:
|
|
668
|
-
"""Process a single lock file and return number of files cleaned."""
|
|
669
536
|
try:
|
|
670
|
-
# Check file age
|
|
671
537
|
file_age_hours = (current_time - lock_file.stat().st_mtime) / 3600
|
|
672
538
|
|
|
673
539
|
if file_age_hours > max_age_hours:
|
|
@@ -683,7 +549,6 @@ class HookLockManager:
|
|
|
683
549
|
def _cleanup_stale_lock_file(
|
|
684
550
|
self, lock_file: Path, max_age_hours: float, current_time: float
|
|
685
551
|
) -> int:
|
|
686
|
-
"""Clean up a stale lock file and return 1 if successful."""
|
|
687
552
|
try:
|
|
688
553
|
with lock_file.open(encoding="utf-8") as f:
|
|
689
554
|
lock_data = json.load(f)
|
|
@@ -698,12 +563,11 @@ class HookLockManager:
|
|
|
698
563
|
hook_name = lock_file.stem
|
|
699
564
|
self._stale_locks_cleaned[hook_name] += 1
|
|
700
565
|
self.logger.info(
|
|
701
|
-
f"Cleaned stale lock: {lock_file} (age: {heartbeat_age_hours
|
|
566
|
+
f"Cleaned stale lock: {lock_file} (age: {heartbeat_age_hours: .2f}h)"
|
|
702
567
|
)
|
|
703
568
|
return 1
|
|
704
569
|
|
|
705
570
|
except (json.JSONDecodeError, KeyError):
|
|
706
|
-
# Corrupted lock file, remove it
|
|
707
571
|
lock_file.unlink()
|
|
708
572
|
self.logger.warning(f"Cleaned corrupted lock file: {lock_file}")
|
|
709
573
|
return 1
|
|
@@ -711,17 +575,12 @@ class HookLockManager:
|
|
|
711
575
|
return 0
|
|
712
576
|
|
|
713
577
|
def get_global_lock_stats(self) -> dict[str, t.Any]:
|
|
714
|
-
"""Get comprehensive statistics about global lock usage.
|
|
715
|
-
|
|
716
|
-
Returns:
|
|
717
|
-
Dictionary containing global lock statistics and metrics
|
|
718
|
-
"""
|
|
719
578
|
stats: dict[str, t.Any] = {
|
|
720
579
|
"global_lock_enabled": self._global_lock_enabled,
|
|
721
580
|
"lock_directory": str(self._global_config.lock_directory),
|
|
722
581
|
"session_id": self._global_config.session_id,
|
|
723
582
|
"hostname": self._global_config.hostname,
|
|
724
|
-
"active_global_locks": list(self._active_global_locks),
|
|
583
|
+
"active_global_locks": list[t.Any](self._active_global_locks),
|
|
725
584
|
"active_heartbeat_tasks": len(self._heartbeat_tasks),
|
|
726
585
|
"configuration": {
|
|
727
586
|
"timeout_seconds": self._global_config.timeout_seconds,
|
|
@@ -734,11 +593,10 @@ class HookLockManager:
|
|
|
734
593
|
"statistics": {},
|
|
735
594
|
}
|
|
736
595
|
|
|
737
|
-
# Per-hook global lock statistics
|
|
738
596
|
all_hooks = (
|
|
739
|
-
set(self._global_lock_attempts.keys())
|
|
740
|
-
| set(self._global_lock_successes.keys())
|
|
741
|
-
| set(self._global_lock_failures.keys())
|
|
597
|
+
set[t.Any](self._global_lock_attempts.keys())
|
|
598
|
+
| set[t.Any](self._global_lock_successes.keys())
|
|
599
|
+
| set[t.Any](self._global_lock_failures.keys())
|
|
742
600
|
)
|
|
743
601
|
|
|
744
602
|
for hook_name in all_hooks:
|
|
@@ -761,7 +619,6 @@ class HookLockManager:
|
|
|
761
619
|
"has_heartbeat_task": hook_name in self._heartbeat_tasks,
|
|
762
620
|
}
|
|
763
621
|
|
|
764
|
-
# Overall statistics
|
|
765
622
|
total_attempts = sum(self._global_lock_attempts.values())
|
|
766
623
|
total_successes = sum(self._global_lock_successes.values())
|
|
767
624
|
total_failures = sum(self._global_lock_failures.values())
|
|
@@ -782,32 +639,21 @@ class HookLockManager:
|
|
|
782
639
|
return stats
|
|
783
640
|
|
|
784
641
|
def configure_from_options(self, options: t.Any) -> None:
|
|
785
|
-
"""Configure the lock manager from CLI options.
|
|
786
|
-
|
|
787
|
-
Args:
|
|
788
|
-
options: Options object containing CLI arguments
|
|
789
|
-
"""
|
|
790
642
|
self._global_config = GlobalLockConfig.from_options(options)
|
|
791
643
|
self._global_lock_enabled = self._global_config.enabled
|
|
792
644
|
|
|
793
|
-
# Apply stale lock cleanup if requested
|
|
794
645
|
if hasattr(options, "global_lock_cleanup") and options.global_lock_cleanup:
|
|
795
646
|
self.cleanup_stale_locks()
|
|
796
647
|
|
|
797
648
|
self.logger.info(
|
|
798
649
|
f"Configured lock manager: global_locks={
|
|
799
650
|
'enabled' if self._global_lock_enabled else 'disabled'
|
|
800
|
-
},"
|
|
651
|
+
}, "
|
|
801
652
|
f" timeout={self._global_config.timeout_seconds}s, "
|
|
802
653
|
f"lock_dir={self._global_config.lock_directory}"
|
|
803
654
|
)
|
|
804
655
|
|
|
805
656
|
def reset_hook_stats(self, hook_name: str | None = None) -> None:
|
|
806
|
-
"""Reset statistics for a specific hook or all hooks.
|
|
807
|
-
|
|
808
|
-
Args:
|
|
809
|
-
hook_name: Name of the hook to reset, or None for all hooks
|
|
810
|
-
"""
|
|
811
657
|
if hook_name:
|
|
812
658
|
self._lock_usage[hook_name].clear()
|
|
813
659
|
self._lock_wait_times[hook_name].clear()
|
|
@@ -824,9 +670,8 @@ class HookLockManager:
|
|
|
824
670
|
self.logger.info("Reset statistics for all hooks")
|
|
825
671
|
|
|
826
672
|
def get_comprehensive_status(self) -> dict[str, t.Any]:
|
|
827
|
-
"""Get comprehensive status including configuration and health."""
|
|
828
673
|
status = {
|
|
829
|
-
"hooks_requiring_locks": list(self._hooks_requiring_locks),
|
|
674
|
+
"hooks_requiring_locks": list[t.Any](self._hooks_requiring_locks),
|
|
830
675
|
"default_timeout": self._default_lock_timeout,
|
|
831
676
|
"custom_timeouts": self._lock_timeouts.copy(),
|
|
832
677
|
"max_history": self._max_history,
|
|
@@ -840,7 +685,6 @@ class HookLockManager:
|
|
|
840
685
|
"total_timeout_failures": sum(self._timeout_failures.values()),
|
|
841
686
|
}
|
|
842
687
|
|
|
843
|
-
# Add global lock information if enabled
|
|
844
688
|
if self._global_lock_enabled:
|
|
845
689
|
status["global_lock_stats"] = self.get_global_lock_stats()
|
|
846
690
|
else:
|
|
@@ -852,5 +696,4 @@ class HookLockManager:
|
|
|
852
696
|
return status
|
|
853
697
|
|
|
854
698
|
|
|
855
|
-
# Singleton instance
|
|
856
699
|
hook_lock_manager = HookLockManager()
|