py-data-engine 0.1.0__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.
- data_engine/__init__.py +37 -0
- data_engine/application/__init__.py +39 -0
- data_engine/application/actions.py +42 -0
- data_engine/application/catalog.py +151 -0
- data_engine/application/control.py +213 -0
- data_engine/application/details.py +73 -0
- data_engine/application/runtime.py +449 -0
- data_engine/application/workspace.py +62 -0
- data_engine/authoring/__init__.py +14 -0
- data_engine/authoring/builder.py +31 -0
- data_engine/authoring/execution/__init__.py +6 -0
- data_engine/authoring/execution/app.py +6 -0
- data_engine/authoring/execution/context.py +82 -0
- data_engine/authoring/execution/continuous.py +176 -0
- data_engine/authoring/execution/grouped.py +106 -0
- data_engine/authoring/execution/logging.py +83 -0
- data_engine/authoring/execution/polling.py +135 -0
- data_engine/authoring/execution/runner.py +210 -0
- data_engine/authoring/execution/single.py +171 -0
- data_engine/authoring/flow.py +361 -0
- data_engine/authoring/helpers.py +160 -0
- data_engine/authoring/model.py +59 -0
- data_engine/authoring/primitives.py +430 -0
- data_engine/authoring/services.py +42 -0
- data_engine/devtools/__init__.py +3 -0
- data_engine/devtools/project_ast_map.py +503 -0
- data_engine/docs/__init__.py +1 -0
- data_engine/docs/sphinx_source/_static/custom.css +13 -0
- data_engine/docs/sphinx_source/api.rst +42 -0
- data_engine/docs/sphinx_source/conf.py +37 -0
- data_engine/docs/sphinx_source/guides/app-runtime-and-workspaces.md +397 -0
- data_engine/docs/sphinx_source/guides/authoring-flow-modules.md +215 -0
- data_engine/docs/sphinx_source/guides/configuring-flows.md +185 -0
- data_engine/docs/sphinx_source/guides/core-concepts.md +208 -0
- data_engine/docs/sphinx_source/guides/database-methods.md +107 -0
- data_engine/docs/sphinx_source/guides/duckdb-helpers.md +462 -0
- data_engine/docs/sphinx_source/guides/flow-context.md +538 -0
- data_engine/docs/sphinx_source/guides/flow-methods.md +206 -0
- data_engine/docs/sphinx_source/guides/getting-started.md +271 -0
- data_engine/docs/sphinx_source/guides/project-inventory.md +5683 -0
- data_engine/docs/sphinx_source/guides/project-map.md +118 -0
- data_engine/docs/sphinx_source/guides/recipes.md +268 -0
- data_engine/docs/sphinx_source/index.rst +22 -0
- data_engine/domain/__init__.py +92 -0
- data_engine/domain/actions.py +69 -0
- data_engine/domain/catalog.py +128 -0
- data_engine/domain/details.py +214 -0
- data_engine/domain/diagnostics.py +56 -0
- data_engine/domain/errors.py +104 -0
- data_engine/domain/inspection.py +99 -0
- data_engine/domain/logs.py +118 -0
- data_engine/domain/operations.py +172 -0
- data_engine/domain/operator.py +72 -0
- data_engine/domain/runs.py +155 -0
- data_engine/domain/runtime.py +279 -0
- data_engine/domain/source_state.py +17 -0
- data_engine/domain/support.py +54 -0
- data_engine/domain/time.py +23 -0
- data_engine/domain/workspace.py +159 -0
- data_engine/flow_modules/__init__.py +1 -0
- data_engine/flow_modules/flow_module_compiler.py +179 -0
- data_engine/flow_modules/flow_module_loader.py +201 -0
- data_engine/helpers/__init__.py +25 -0
- data_engine/helpers/duckdb.py +705 -0
- data_engine/hosts/__init__.py +1 -0
- data_engine/hosts/daemon/__init__.py +23 -0
- data_engine/hosts/daemon/app.py +221 -0
- data_engine/hosts/daemon/bootstrap.py +69 -0
- data_engine/hosts/daemon/client.py +465 -0
- data_engine/hosts/daemon/commands.py +64 -0
- data_engine/hosts/daemon/composition.py +310 -0
- data_engine/hosts/daemon/constants.py +15 -0
- data_engine/hosts/daemon/entrypoints.py +97 -0
- data_engine/hosts/daemon/lifecycle.py +191 -0
- data_engine/hosts/daemon/manager.py +272 -0
- data_engine/hosts/daemon/ownership.py +126 -0
- data_engine/hosts/daemon/runtime_commands.py +188 -0
- data_engine/hosts/daemon/runtime_control.py +31 -0
- data_engine/hosts/daemon/server.py +84 -0
- data_engine/hosts/daemon/shared_state.py +147 -0
- data_engine/hosts/daemon/state_sync.py +101 -0
- data_engine/platform/__init__.py +1 -0
- data_engine/platform/identity.py +35 -0
- data_engine/platform/local_settings.py +146 -0
- data_engine/platform/theme.py +259 -0
- data_engine/platform/workspace_models.py +190 -0
- data_engine/platform/workspace_policy.py +333 -0
- data_engine/runtime/__init__.py +1 -0
- data_engine/runtime/file_watch.py +185 -0
- data_engine/runtime/ledger_models.py +116 -0
- data_engine/runtime/runtime_db.py +938 -0
- data_engine/runtime/shared_state.py +523 -0
- data_engine/services/__init__.py +49 -0
- data_engine/services/daemon.py +64 -0
- data_engine/services/daemon_state.py +40 -0
- data_engine/services/flow_catalog.py +102 -0
- data_engine/services/flow_execution.py +48 -0
- data_engine/services/ledger.py +85 -0
- data_engine/services/logs.py +65 -0
- data_engine/services/runtime_binding.py +105 -0
- data_engine/services/runtime_execution.py +126 -0
- data_engine/services/runtime_history.py +62 -0
- data_engine/services/settings.py +58 -0
- data_engine/services/shared_state.py +28 -0
- data_engine/services/theme.py +59 -0
- data_engine/services/workspace_provisioning.py +224 -0
- data_engine/services/workspaces.py +74 -0
- data_engine/ui/__init__.py +3 -0
- data_engine/ui/cli/__init__.py +19 -0
- data_engine/ui/cli/app.py +161 -0
- data_engine/ui/cli/commands_doctor.py +178 -0
- data_engine/ui/cli/commands_run.py +80 -0
- data_engine/ui/cli/commands_start.py +100 -0
- data_engine/ui/cli/commands_workspace.py +97 -0
- data_engine/ui/cli/dependencies.py +44 -0
- data_engine/ui/cli/parser.py +56 -0
- data_engine/ui/gui/__init__.py +25 -0
- data_engine/ui/gui/app.py +116 -0
- data_engine/ui/gui/bootstrap.py +487 -0
- data_engine/ui/gui/bootstrapper.py +140 -0
- data_engine/ui/gui/cache_models.py +23 -0
- data_engine/ui/gui/control_support.py +185 -0
- data_engine/ui/gui/controllers/__init__.py +6 -0
- data_engine/ui/gui/controllers/flows.py +439 -0
- data_engine/ui/gui/controllers/runtime.py +245 -0
- data_engine/ui/gui/dialogs/__init__.py +12 -0
- data_engine/ui/gui/dialogs/messages.py +88 -0
- data_engine/ui/gui/dialogs/previews.py +222 -0
- data_engine/ui/gui/helpers/__init__.py +62 -0
- data_engine/ui/gui/helpers/inspection.py +81 -0
- data_engine/ui/gui/helpers/lifecycle.py +112 -0
- data_engine/ui/gui/helpers/scroll.py +28 -0
- data_engine/ui/gui/helpers/theming.py +87 -0
- data_engine/ui/gui/icons/dark_light.svg +12 -0
- data_engine/ui/gui/icons/documentation.svg +1 -0
- data_engine/ui/gui/icons/failed.svg +3 -0
- data_engine/ui/gui/icons/group.svg +4 -0
- data_engine/ui/gui/icons/home.svg +2 -0
- data_engine/ui/gui/icons/manual.svg +2 -0
- data_engine/ui/gui/icons/poll.svg +2 -0
- data_engine/ui/gui/icons/schedule.svg +4 -0
- data_engine/ui/gui/icons/settings.svg +2 -0
- data_engine/ui/gui/icons/started.svg +3 -0
- data_engine/ui/gui/icons/success.svg +3 -0
- data_engine/ui/gui/icons/view-log.svg +3 -0
- data_engine/ui/gui/icons.py +50 -0
- data_engine/ui/gui/launcher.py +48 -0
- data_engine/ui/gui/presenters/__init__.py +72 -0
- data_engine/ui/gui/presenters/docs.py +140 -0
- data_engine/ui/gui/presenters/logs.py +58 -0
- data_engine/ui/gui/presenters/runtime_projection.py +29 -0
- data_engine/ui/gui/presenters/sidebar.py +88 -0
- data_engine/ui/gui/presenters/steps.py +148 -0
- data_engine/ui/gui/presenters/workspace.py +39 -0
- data_engine/ui/gui/presenters/workspace_binding.py +75 -0
- data_engine/ui/gui/presenters/workspace_settings.py +182 -0
- data_engine/ui/gui/preview_models.py +37 -0
- data_engine/ui/gui/render_support.py +241 -0
- data_engine/ui/gui/rendering/__init__.py +12 -0
- data_engine/ui/gui/rendering/artifacts.py +95 -0
- data_engine/ui/gui/rendering/icons.py +50 -0
- data_engine/ui/gui/runtime.py +47 -0
- data_engine/ui/gui/state_support.py +193 -0
- data_engine/ui/gui/support.py +214 -0
- data_engine/ui/gui/surface.py +209 -0
- data_engine/ui/gui/theme.py +720 -0
- data_engine/ui/gui/widgets/__init__.py +34 -0
- data_engine/ui/gui/widgets/config.py +41 -0
- data_engine/ui/gui/widgets/logs.py +62 -0
- data_engine/ui/gui/widgets/panels.py +507 -0
- data_engine/ui/gui/widgets/sidebar.py +130 -0
- data_engine/ui/gui/widgets/steps.py +84 -0
- data_engine/ui/tui/__init__.py +5 -0
- data_engine/ui/tui/app.py +222 -0
- data_engine/ui/tui/bootstrap.py +475 -0
- data_engine/ui/tui/bootstrapper.py +117 -0
- data_engine/ui/tui/controllers/__init__.py +6 -0
- data_engine/ui/tui/controllers/flows.py +349 -0
- data_engine/ui/tui/controllers/runtime.py +167 -0
- data_engine/ui/tui/runtime.py +34 -0
- data_engine/ui/tui/state_support.py +141 -0
- data_engine/ui/tui/support.py +63 -0
- data_engine/ui/tui/theme.py +204 -0
- data_engine/ui/tui/widgets.py +123 -0
- data_engine/views/__init__.py +109 -0
- data_engine/views/actions.py +80 -0
- data_engine/views/artifacts.py +58 -0
- data_engine/views/flow_display.py +69 -0
- data_engine/views/logs.py +54 -0
- data_engine/views/models.py +96 -0
- data_engine/views/presentation.py +133 -0
- data_engine/views/runs.py +62 -0
- data_engine/views/state.py +39 -0
- data_engine/views/status.py +13 -0
- data_engine/views/text.py +109 -0
- py_data_engine-0.1.0.dist-info/METADATA +330 -0
- py_data_engine-0.1.0.dist-info/RECORD +200 -0
- py_data_engine-0.1.0.dist-info/WHEEL +5 -0
- py_data_engine-0.1.0.dist-info/entry_points.txt +2 -0
- py_data_engine-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Composition helpers for the daemon host."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
import threading
|
|
8
|
+
from uuid import uuid4
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from data_engine.hosts.daemon.shared_state import DaemonSharedStateAdapter
|
|
12
|
+
from data_engine.platform.workspace_models import WorkspacePaths, machine_id_text
|
|
13
|
+
from data_engine.runtime.runtime_db import RuntimeLedger
|
|
14
|
+
from data_engine.services.flow_catalog import FlowCatalogService
|
|
15
|
+
from data_engine.services.flow_execution import FlowExecutionService
|
|
16
|
+
from data_engine.services.ledger import LedgerService
|
|
17
|
+
from data_engine.services.runtime_execution import RuntimeExecutionService
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class DaemonHostDependencyFactories:
|
|
22
|
+
"""Constructor seam for daemon host collaborators."""
|
|
23
|
+
|
|
24
|
+
flow_catalog_service_factory: Callable[[], FlowCatalogService]
|
|
25
|
+
flow_execution_service_factory: Callable[[], FlowExecutionService]
|
|
26
|
+
runtime_execution_service_factory: Callable[[], RuntimeExecutionService]
|
|
27
|
+
shared_state_adapter_factory: Callable[[], DaemonSharedStateAdapter] = field(default=DaemonSharedStateAdapter)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def default_daemon_host_dependency_factories() -> DaemonHostDependencyFactories:
|
|
31
|
+
"""Build the default daemon-host constructor bundle."""
|
|
32
|
+
return DaemonHostDependencyFactories(
|
|
33
|
+
flow_catalog_service_factory=FlowCatalogService,
|
|
34
|
+
flow_execution_service_factory=FlowExecutionService,
|
|
35
|
+
runtime_execution_service_factory=RuntimeExecutionService,
|
|
36
|
+
shared_state_adapter_factory=DaemonSharedStateAdapter,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class DaemonHostDependencies:
|
|
42
|
+
"""Concrete collaborators used by one daemon host instance."""
|
|
43
|
+
|
|
44
|
+
runtime_ledger: RuntimeLedger
|
|
45
|
+
flow_catalog_service: FlowCatalogService
|
|
46
|
+
flow_execution_service: FlowExecutionService
|
|
47
|
+
runtime_execution_service: RuntimeExecutionService
|
|
48
|
+
shared_state_adapter: DaemonSharedStateAdapter
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def build_default(
|
|
52
|
+
cls,
|
|
53
|
+
paths: WorkspacePaths,
|
|
54
|
+
*,
|
|
55
|
+
ledger_service: LedgerService | None = None,
|
|
56
|
+
factories: DaemonHostDependencyFactories | None = None,
|
|
57
|
+
) -> "DaemonHostDependencies":
|
|
58
|
+
"""Build the default dependency bundle for one workspace host."""
|
|
59
|
+
ledger_service = ledger_service or LedgerService()
|
|
60
|
+
factories = factories or default_daemon_host_dependency_factories()
|
|
61
|
+
return cls(
|
|
62
|
+
runtime_ledger=ledger_service.open_for_workspace(paths.workspace_root),
|
|
63
|
+
flow_catalog_service=factories.flow_catalog_service_factory(),
|
|
64
|
+
flow_execution_service=factories.flow_execution_service_factory(),
|
|
65
|
+
runtime_execution_service=factories.runtime_execution_service_factory(),
|
|
66
|
+
shared_state_adapter=factories.shared_state_adapter_factory(),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class DaemonHostIdentity:
|
|
72
|
+
"""Process and machine identity for one daemon host instance."""
|
|
73
|
+
|
|
74
|
+
machine_id: str
|
|
75
|
+
daemon_id: str
|
|
76
|
+
pid: int
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def current_process(cls) -> "DaemonHostIdentity":
|
|
80
|
+
"""Build the current-process identity for one daemon host."""
|
|
81
|
+
return cls(
|
|
82
|
+
machine_id=machine_id_text(),
|
|
83
|
+
daemon_id=uuid4().hex,
|
|
84
|
+
pid=os.getpid(),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class DaemonHostState:
|
|
90
|
+
"""Mutable state for a fresh daemon host instance."""
|
|
91
|
+
|
|
92
|
+
status: str
|
|
93
|
+
last_checkpoint_at_utc: str
|
|
94
|
+
workspace_owned: bool
|
|
95
|
+
leased_by_machine_id: str | None
|
|
96
|
+
runtime_active: bool
|
|
97
|
+
runtime_stopping: bool
|
|
98
|
+
engine_starting: bool
|
|
99
|
+
engine_thread: threading.Thread | None
|
|
100
|
+
engine_runtime_stop_event: threading.Event
|
|
101
|
+
engine_flow_stop_event: threading.Event
|
|
102
|
+
pending_manual_run_names: set[str]
|
|
103
|
+
manual_run_threads: dict[str, threading.Thread]
|
|
104
|
+
manual_stop_events: dict[str, threading.Event]
|
|
105
|
+
shutdown_event: threading.Event
|
|
106
|
+
checkpoint_thread: threading.Thread | None
|
|
107
|
+
consecutive_checkpoint_failures: int
|
|
108
|
+
listener: object | None
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def build(cls, *, started_at_utc: str) -> "DaemonHostState":
|
|
112
|
+
"""Build the default mutable state for a fresh daemon host."""
|
|
113
|
+
return cls(
|
|
114
|
+
status="starting",
|
|
115
|
+
last_checkpoint_at_utc=started_at_utc,
|
|
116
|
+
workspace_owned=False,
|
|
117
|
+
leased_by_machine_id=None,
|
|
118
|
+
runtime_active=False,
|
|
119
|
+
runtime_stopping=False,
|
|
120
|
+
engine_starting=False,
|
|
121
|
+
engine_thread=None,
|
|
122
|
+
engine_runtime_stop_event=threading.Event(),
|
|
123
|
+
engine_flow_stop_event=threading.Event(),
|
|
124
|
+
pending_manual_run_names=set(),
|
|
125
|
+
manual_run_threads={},
|
|
126
|
+
manual_stop_events={},
|
|
127
|
+
shutdown_event=threading.Event(),
|
|
128
|
+
checkpoint_thread=None,
|
|
129
|
+
consecutive_checkpoint_failures=0,
|
|
130
|
+
listener=None,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def claim_workspace(self) -> None:
|
|
134
|
+
"""Mark the current daemon as owning the workspace."""
|
|
135
|
+
self.workspace_owned = True
|
|
136
|
+
self.leased_by_machine_id = None
|
|
137
|
+
self.status = "idle"
|
|
138
|
+
|
|
139
|
+
def release_workspace(self, *, leased_by_machine_id: str | None = None, status: str | None = None) -> None:
|
|
140
|
+
"""Mark the current daemon as no longer owning the workspace."""
|
|
141
|
+
self.workspace_owned = False
|
|
142
|
+
self.leased_by_machine_id = leased_by_machine_id
|
|
143
|
+
if status is not None:
|
|
144
|
+
self.status = status
|
|
145
|
+
|
|
146
|
+
def begin_runtime(self, *, status: str = "running") -> None:
|
|
147
|
+
"""Mark the engine runtime as active and running."""
|
|
148
|
+
self.engine_starting = False
|
|
149
|
+
self.runtime_active = True
|
|
150
|
+
self.runtime_stopping = False
|
|
151
|
+
self.status = status
|
|
152
|
+
|
|
153
|
+
def stop_runtime(self, *, status: str = "stopping") -> None:
|
|
154
|
+
"""Mark the engine runtime as stopping."""
|
|
155
|
+
self.engine_starting = False
|
|
156
|
+
self.runtime_stopping = True
|
|
157
|
+
self.status = status
|
|
158
|
+
|
|
159
|
+
def end_runtime(self, *, status: str = "idle") -> None:
|
|
160
|
+
"""Mark the engine runtime as inactive."""
|
|
161
|
+
self.engine_starting = False
|
|
162
|
+
self.runtime_active = False
|
|
163
|
+
self.runtime_stopping = False
|
|
164
|
+
if self.status != "failed":
|
|
165
|
+
self.status = status
|
|
166
|
+
|
|
167
|
+
def set_checkpoint_time(self, checkpoint_at_utc: str, *, status: str | None = None) -> None:
|
|
168
|
+
"""Update the last successful checkpoint timestamp."""
|
|
169
|
+
self.last_checkpoint_at_utc = checkpoint_at_utc
|
|
170
|
+
if status is not None:
|
|
171
|
+
self.status = status
|
|
172
|
+
|
|
173
|
+
def set_leased_by_machine_id(self, machine_id: str | None) -> None:
|
|
174
|
+
"""Update the current lease owner identifier."""
|
|
175
|
+
self.leased_by_machine_id = machine_id
|
|
176
|
+
|
|
177
|
+
def increment_checkpoint_failures(self) -> int:
|
|
178
|
+
"""Increment the repeated-checkpoint failure counter."""
|
|
179
|
+
self.consecutive_checkpoint_failures += 1
|
|
180
|
+
return self.consecutive_checkpoint_failures
|
|
181
|
+
|
|
182
|
+
def reset_checkpoint_failures(self) -> None:
|
|
183
|
+
"""Reset the repeated-checkpoint failure counter."""
|
|
184
|
+
self.consecutive_checkpoint_failures = 0
|
|
185
|
+
|
|
186
|
+
def set_engine_threads(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
runtime_stop_event: threading.Event,
|
|
190
|
+
flow_stop_event: threading.Event,
|
|
191
|
+
engine_thread: threading.Thread | None = None,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Replace the active engine coordination objects."""
|
|
194
|
+
self.engine_runtime_stop_event = runtime_stop_event
|
|
195
|
+
self.engine_flow_stop_event = flow_stop_event
|
|
196
|
+
self.engine_thread = engine_thread
|
|
197
|
+
|
|
198
|
+
def reserve_engine_start(self) -> bool:
|
|
199
|
+
"""Reserve engine startup so concurrent start requests collapse to one attempt."""
|
|
200
|
+
if self.runtime_active or self.engine_starting:
|
|
201
|
+
return False
|
|
202
|
+
self.engine_starting = True
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
def clear_engine_start_reservation(self) -> None:
|
|
206
|
+
"""Clear any in-progress engine startup reservation."""
|
|
207
|
+
self.engine_starting = False
|
|
208
|
+
|
|
209
|
+
def reserve_manual_run(self, name: str) -> bool:
|
|
210
|
+
"""Reserve one manual run name before flow loading starts."""
|
|
211
|
+
if name in self.pending_manual_run_names:
|
|
212
|
+
return False
|
|
213
|
+
self.pending_manual_run_names.add(name)
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
def clear_manual_run_reservation(self, name: str) -> None:
|
|
217
|
+
"""Clear one in-progress manual run reservation."""
|
|
218
|
+
self.pending_manual_run_names.discard(name)
|
|
219
|
+
|
|
220
|
+
def register_manual_run(self, name: str, *, thread: threading.Thread, stop_event: threading.Event) -> None:
|
|
221
|
+
"""Register one manual run and its stop signal."""
|
|
222
|
+
self.pending_manual_run_names.discard(name)
|
|
223
|
+
self.manual_run_threads[name] = thread
|
|
224
|
+
self.manual_stop_events[name] = stop_event
|
|
225
|
+
|
|
226
|
+
def unregister_manual_run(self, name: str) -> None:
|
|
227
|
+
"""Remove one completed manual run."""
|
|
228
|
+
self.pending_manual_run_names.discard(name)
|
|
229
|
+
self.manual_run_threads.pop(name, None)
|
|
230
|
+
self.manual_stop_events.pop(name, None)
|
|
231
|
+
|
|
232
|
+
def set_listener(self, listener: object | None) -> None:
|
|
233
|
+
"""Update the active listener object."""
|
|
234
|
+
self.listener = listener
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class DaemonHostFacade:
|
|
238
|
+
"""Explicit high-level host-state facade over the mutable daemon state object."""
|
|
239
|
+
|
|
240
|
+
def __init__(self, state: DaemonHostState) -> None:
|
|
241
|
+
self.state = state
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def status(self) -> str:
|
|
245
|
+
return self.state.status
|
|
246
|
+
|
|
247
|
+
@status.setter
|
|
248
|
+
def status(self, value: str) -> None:
|
|
249
|
+
self.state.status = value
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def workspace_owned(self) -> bool:
|
|
253
|
+
return self.state.workspace_owned
|
|
254
|
+
|
|
255
|
+
@workspace_owned.setter
|
|
256
|
+
def workspace_owned(self, value: bool) -> None:
|
|
257
|
+
self.state.workspace_owned = value
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def leased_by_machine_id(self) -> str | None:
|
|
261
|
+
return self.state.leased_by_machine_id
|
|
262
|
+
|
|
263
|
+
@leased_by_machine_id.setter
|
|
264
|
+
def leased_by_machine_id(self, value: str | None) -> None:
|
|
265
|
+
self.state.leased_by_machine_id = value
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def runtime_active(self) -> bool:
|
|
269
|
+
return self.state.runtime_active
|
|
270
|
+
|
|
271
|
+
@runtime_active.setter
|
|
272
|
+
def runtime_active(self, value: bool) -> None:
|
|
273
|
+
self.state.runtime_active = value
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def runtime_stopping(self) -> bool:
|
|
277
|
+
return self.state.runtime_stopping
|
|
278
|
+
|
|
279
|
+
@runtime_stopping.setter
|
|
280
|
+
def runtime_stopping(self, value: bool) -> None:
|
|
281
|
+
self.state.runtime_stopping = value
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def shutdown_event(self) -> threading.Event:
|
|
285
|
+
return self.state.shutdown_event
|
|
286
|
+
|
|
287
|
+
@shutdown_event.setter
|
|
288
|
+
def shutdown_event(self, value: threading.Event) -> None:
|
|
289
|
+
self.state.shutdown_event = value
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def listener(self) -> object | None:
|
|
293
|
+
return self.state.listener
|
|
294
|
+
|
|
295
|
+
@listener.setter
|
|
296
|
+
def listener(self, value: object | None) -> None:
|
|
297
|
+
self.state.listener = value
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
__all__ = [
|
|
301
|
+
"DaemonHostFacade",
|
|
302
|
+
"DaemonHostDependencyFactories",
|
|
303
|
+
"DaemonHostDependencies",
|
|
304
|
+
"DaemonHostIdentity",
|
|
305
|
+
"DaemonHostState",
|
|
306
|
+
"default_daemon_host_dependency_factories",
|
|
307
|
+
]
|
|
308
|
+
|
|
309
|
+
# Backward-compatible alias while the daemon host moves to a single grouped state object.
|
|
310
|
+
DaemonHostInitialState = DaemonHostState
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Shared daemon host constants."""
|
|
2
|
+
|
|
3
|
+
APP_VERSION = "0.1.0"
|
|
4
|
+
CHECKPOINT_INTERVAL_SECONDS = 30.0
|
|
5
|
+
CONTROL_REQUEST_POLL_INTERVAL_SECONDS = 1.0
|
|
6
|
+
STALE_AFTER_SECONDS = 90.0
|
|
7
|
+
DAEMON_STARTUP_LOCK_STALE_SECONDS = 15.0
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"APP_VERSION",
|
|
11
|
+
"CHECKPOINT_INTERVAL_SECONDS",
|
|
12
|
+
"CONTROL_REQUEST_POLL_INTERVAL_SECONDS",
|
|
13
|
+
"DAEMON_STARTUP_LOCK_STALE_SECONDS",
|
|
14
|
+
"STALE_AFTER_SECONDS",
|
|
15
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Module entrypoints for launching one workspace daemon process."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
from data_engine.domain import DaemonLifecyclePolicy
|
|
11
|
+
from data_engine.hosts.daemon.server import serve_workspace_daemon as serve_daemon_process
|
|
12
|
+
from data_engine.platform.workspace_models import (
|
|
13
|
+
DATA_ENGINE_APP_ROOT_ENV_VAR,
|
|
14
|
+
DATA_ENGINE_WORKSPACE_ID_ENV_VAR,
|
|
15
|
+
DATA_ENGINE_WORKSPACE_ROOT_ENV_VAR,
|
|
16
|
+
)
|
|
17
|
+
from data_engine.services import WorkspaceService
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_workspace_service_factory() -> WorkspaceService:
|
|
21
|
+
"""Build the default workspace-service collaborator for daemon entrypoints."""
|
|
22
|
+
return WorkspaceService()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def serve_workspace_daemon(
|
|
26
|
+
service_type,
|
|
27
|
+
*,
|
|
28
|
+
workspace_root: Path | None = None,
|
|
29
|
+
workspace_id: str | None = None,
|
|
30
|
+
lifecycle_policy: DaemonLifecyclePolicy = DaemonLifecyclePolicy.PERSISTENT,
|
|
31
|
+
workspace_service: WorkspaceService | None = None,
|
|
32
|
+
resolve_paths_func=None,
|
|
33
|
+
) -> int:
|
|
34
|
+
"""Start serving one workspace daemon in the current process."""
|
|
35
|
+
return serve_daemon_process(
|
|
36
|
+
service_type,
|
|
37
|
+
workspace_root=workspace_root,
|
|
38
|
+
workspace_id=workspace_id,
|
|
39
|
+
lifecycle_policy=lifecycle_policy,
|
|
40
|
+
workspace_service=workspace_service,
|
|
41
|
+
resolve_paths_func=resolve_paths_func,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
46
|
+
"""Build the daemon module parser."""
|
|
47
|
+
parser = argparse.ArgumentParser(description="Run one Data Engine workspace daemon.")
|
|
48
|
+
parser.add_argument("--workspace", type=Path, required=True, help="Authored workspace root to host.")
|
|
49
|
+
parser.add_argument("--app-root", type=Path, default=None, help="Data Engine app root used for local artifacts.")
|
|
50
|
+
parser.add_argument("--workspace-id", default=None, help="Explicit workspace id override.")
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--lifecycle-policy",
|
|
53
|
+
choices=tuple(policy.value for policy in DaemonLifecyclePolicy),
|
|
54
|
+
default=DaemonLifecyclePolicy.PERSISTENT.value,
|
|
55
|
+
help="Daemon lifetime policy.",
|
|
56
|
+
)
|
|
57
|
+
return parser
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def main(
|
|
61
|
+
service_type,
|
|
62
|
+
argv: list[str] | None = None,
|
|
63
|
+
*,
|
|
64
|
+
workspace_service: WorkspaceService | None = None,
|
|
65
|
+
workspace_service_factory: Callable[[], WorkspaceService] | None = None,
|
|
66
|
+
resolve_paths_func=None,
|
|
67
|
+
serve_workspace_daemon_func=None,
|
|
68
|
+
) -> int:
|
|
69
|
+
"""Run the daemon module entrypoint for one concrete daemon service type."""
|
|
70
|
+
parser = build_parser()
|
|
71
|
+
args = parser.parse_args(argv)
|
|
72
|
+
if args.app_root is not None:
|
|
73
|
+
os.environ[DATA_ENGINE_APP_ROOT_ENV_VAR] = str(args.app_root.expanduser().resolve())
|
|
74
|
+
os.environ[DATA_ENGINE_WORKSPACE_ROOT_ENV_VAR] = str(args.workspace.expanduser().resolve())
|
|
75
|
+
if args.workspace_id:
|
|
76
|
+
os.environ[DATA_ENGINE_WORKSPACE_ID_ENV_VAR] = args.workspace_id
|
|
77
|
+
if resolve_paths_func is None:
|
|
78
|
+
workspace_service = workspace_service or (workspace_service_factory or default_workspace_service_factory)()
|
|
79
|
+
resolve_paths_func = workspace_service.resolve_paths
|
|
80
|
+
paths = resolve_paths_func(workspace_root=args.workspace, workspace_id=args.workspace_id)
|
|
81
|
+
serve_workspace_daemon_func = serve_workspace_daemon_func or serve_workspace_daemon
|
|
82
|
+
return serve_workspace_daemon_func(
|
|
83
|
+
service_type,
|
|
84
|
+
workspace_root=paths.workspace_root,
|
|
85
|
+
workspace_id=paths.workspace_id,
|
|
86
|
+
lifecycle_policy=args.lifecycle_policy,
|
|
87
|
+
workspace_service=workspace_service,
|
|
88
|
+
resolve_paths_func=resolve_paths_func,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = [
|
|
93
|
+
"build_parser",
|
|
94
|
+
"default_workspace_service_factory",
|
|
95
|
+
"main",
|
|
96
|
+
"serve_workspace_daemon",
|
|
97
|
+
]
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Lifecycle and checkpoint policy for the daemon host."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import time
|
|
7
|
+
import traceback
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from data_engine.domain import DaemonLifecyclePolicy
|
|
11
|
+
from data_engine.hosts.daemon.ownership import (
|
|
12
|
+
honor_control_request_if_needed,
|
|
13
|
+
release_workspace_claim,
|
|
14
|
+
try_claim_requested_control,
|
|
15
|
+
)
|
|
16
|
+
from data_engine.hosts.daemon.constants import (
|
|
17
|
+
CHECKPOINT_INTERVAL_SECONDS,
|
|
18
|
+
CONTROL_REQUEST_POLL_INTERVAL_SECONDS,
|
|
19
|
+
)
|
|
20
|
+
from data_engine.hosts.daemon.runtime_control import stop_active_work
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from data_engine.hosts.daemon.app import DataEngineDaemonService
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def checkpoint_loop(service: "DataEngineDaemonService") -> None:
|
|
27
|
+
next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
|
|
28
|
+
while not service.host.shutdown_event.wait(CONTROL_REQUEST_POLL_INTERVAL_SECONDS):
|
|
29
|
+
if _should_shutdown_for_missing_clients(service):
|
|
30
|
+
service._debug_log("no live local clients remain; shutting down ephemeral daemon")
|
|
31
|
+
relinquish_workspace_for_missing_clients(service)
|
|
32
|
+
break
|
|
33
|
+
if not service._workspace_root_is_available():
|
|
34
|
+
service._debug_log("workspace root no longer available; shutting down daemon")
|
|
35
|
+
relinquish_workspace_for_missing_root(service)
|
|
36
|
+
break
|
|
37
|
+
with service._state_lock:
|
|
38
|
+
workspace_owned = service.host.workspace_owned
|
|
39
|
+
if not workspace_owned:
|
|
40
|
+
try:
|
|
41
|
+
if try_claim_requested_control(service):
|
|
42
|
+
next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
|
|
43
|
+
continue
|
|
44
|
+
service._refresh_observer_snapshot()
|
|
45
|
+
except Exception:
|
|
46
|
+
pass
|
|
47
|
+
continue
|
|
48
|
+
try:
|
|
49
|
+
if honor_control_request_if_needed(service):
|
|
50
|
+
next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
|
|
51
|
+
continue
|
|
52
|
+
if time.monotonic() < next_checkpoint_at:
|
|
53
|
+
continue
|
|
54
|
+
with service._state_lock:
|
|
55
|
+
status = "degraded" if service.state.consecutive_checkpoint_failures >= 1 else service.host.status
|
|
56
|
+
service._checkpoint_once(status=status)
|
|
57
|
+
with service._state_lock:
|
|
58
|
+
service.state.consecutive_checkpoint_failures = 0
|
|
59
|
+
next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
|
|
60
|
+
except Exception:
|
|
61
|
+
service._debug_log("checkpoint failed")
|
|
62
|
+
service._debug_log(traceback.format_exc().rstrip())
|
|
63
|
+
with service._state_lock:
|
|
64
|
+
failure_count = service.state.increment_checkpoint_failures()
|
|
65
|
+
if failure_count == 2:
|
|
66
|
+
with service._state_lock:
|
|
67
|
+
service.state.status = "degraded"
|
|
68
|
+
service._update_daemon_state(status="degraded")
|
|
69
|
+
service._debug_log("daemon marked degraded after repeated checkpoint failures")
|
|
70
|
+
if failure_count >= 3:
|
|
71
|
+
service._debug_log("relinquishing workspace after repeated checkpoint failures")
|
|
72
|
+
relinquish_workspace_after_checkpoint_failures(service)
|
|
73
|
+
next_checkpoint_at = time.monotonic() + CHECKPOINT_INTERVAL_SECONDS
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def relinquish_workspace_after_checkpoint_failures(service: "DataEngineDaemonService") -> None:
|
|
77
|
+
"""Stop active work, release shared ownership, and stop the daemon."""
|
|
78
|
+
with service._state_lock:
|
|
79
|
+
service.state.stop_runtime(status="failed")
|
|
80
|
+
service._debug_log("relinquish workspace starting")
|
|
81
|
+
stop_active_work(service)
|
|
82
|
+
release_workspace_claim(service, status="failed", update_state=True)
|
|
83
|
+
service._debug_log("relinquish workspace complete")
|
|
84
|
+
shutdown_if_unowned_and_idle(service, reason="checkpoint failures")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def relinquish_workspace_for_control_request(service: "DataEngineDaemonService", requester_machine_id: str) -> None:
|
|
88
|
+
"""Stop active work, hand ownership off, and stop this daemon."""
|
|
89
|
+
with service._state_lock:
|
|
90
|
+
service.state.stop_runtime(status="stopping flow")
|
|
91
|
+
service._debug_log(f"relinquish for control request requester={requester_machine_id}")
|
|
92
|
+
stop_active_work(service)
|
|
93
|
+
release_workspace_claim(
|
|
94
|
+
service,
|
|
95
|
+
leased_by_machine_id=requester_machine_id,
|
|
96
|
+
status="leased",
|
|
97
|
+
update_state=True,
|
|
98
|
+
)
|
|
99
|
+
service._debug_log("relinquish for control request complete")
|
|
100
|
+
shutdown_if_unowned_and_idle(service, reason="control request handoff")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def relinquish_workspace_for_missing_root(service: "DataEngineDaemonService") -> None:
|
|
104
|
+
"""Stop active work and exit when the authored workspace root disappears."""
|
|
105
|
+
with service._state_lock:
|
|
106
|
+
service.state.stop_runtime(status="workspace missing")
|
|
107
|
+
stop_active_work(service)
|
|
108
|
+
release_workspace_claim(service, status="workspace missing")
|
|
109
|
+
service.host.shutdown_event.set()
|
|
110
|
+
service._wake_listener()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def relinquish_workspace_for_missing_clients(service: "DataEngineDaemonService") -> None:
|
|
114
|
+
"""Stop active work and exit when an ephemeral daemon has no live local clients."""
|
|
115
|
+
with service._state_lock:
|
|
116
|
+
service.state.stop_runtime(status="client disconnected")
|
|
117
|
+
stop_active_work(service)
|
|
118
|
+
release_workspace_claim(service, status="client disconnected")
|
|
119
|
+
service.host.shutdown_event.set()
|
|
120
|
+
service._wake_listener()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _should_shutdown_for_missing_clients(service: "DataEngineDaemonService") -> bool:
|
|
124
|
+
"""Return whether an ephemeral daemon should stop because no live clients remain."""
|
|
125
|
+
if service.lifecycle_policy is not DaemonLifecyclePolicy.EPHEMERAL:
|
|
126
|
+
return False
|
|
127
|
+
with service._state_lock:
|
|
128
|
+
if service.host.runtime_active or service.host.runtime_stopping:
|
|
129
|
+
return False
|
|
130
|
+
if service.state.manual_run_threads:
|
|
131
|
+
return False
|
|
132
|
+
try:
|
|
133
|
+
return service.runtime_ledger.count_live_client_sessions(service.paths.workspace_id) == 0
|
|
134
|
+
except Exception:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def shutdown_if_unowned_and_idle(service: "DataEngineDaemonService", *, reason: str) -> None:
|
|
139
|
+
"""Exit when this daemon no longer owns the workspace and has no active work."""
|
|
140
|
+
with service._state_lock:
|
|
141
|
+
if service.host.workspace_owned:
|
|
142
|
+
return
|
|
143
|
+
if service.host.runtime_active or service.host.runtime_stopping:
|
|
144
|
+
return
|
|
145
|
+
if service.state.manual_run_threads:
|
|
146
|
+
return
|
|
147
|
+
service._debug_log(f"shutdown requested reason={reason}")
|
|
148
|
+
service.host.shutdown_event.set()
|
|
149
|
+
service._wake_listener()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def shutdown(service: "DataEngineDaemonService") -> None:
|
|
153
|
+
service._debug_log("shutdown starting")
|
|
154
|
+
stop_active_work(service)
|
|
155
|
+
if service.state.checkpoint_thread is not None and service.state.checkpoint_thread.is_alive():
|
|
156
|
+
service.state.checkpoint_thread.join(timeout=5.0)
|
|
157
|
+
with service._state_lock:
|
|
158
|
+
workspace_owned = service.host.workspace_owned
|
|
159
|
+
status = service.host.status
|
|
160
|
+
if workspace_owned and status not in {"failed", "workspace missing"}:
|
|
161
|
+
try:
|
|
162
|
+
service._checkpoint_once(status="stopping")
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
release_workspace_claim(service)
|
|
166
|
+
try:
|
|
167
|
+
service.runtime_ledger.clear_daemon_state(service.paths.workspace_id)
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
service.runtime_ledger.close()
|
|
171
|
+
if service.host.listener is not None:
|
|
172
|
+
try:
|
|
173
|
+
service.host.listener.close()
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
if service.paths.daemon_endpoint_kind == "unix":
|
|
177
|
+
try:
|
|
178
|
+
Path(service.paths.daemon_endpoint_path).unlink()
|
|
179
|
+
except FileNotFoundError:
|
|
180
|
+
pass
|
|
181
|
+
service._debug_log("shutdown complete")
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
__all__ = [
|
|
185
|
+
"checkpoint_loop",
|
|
186
|
+
"relinquish_workspace_after_checkpoint_failures",
|
|
187
|
+
"relinquish_workspace_for_control_request",
|
|
188
|
+
"relinquish_workspace_for_missing_root",
|
|
189
|
+
"shutdown",
|
|
190
|
+
"shutdown_if_unowned_and_idle",
|
|
191
|
+
]
|