Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion clawmetry/asimov/daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ def journal_directory():
Path.home() / ".clawmetry" / "robotics" / "journal").expanduser()


def daemon_writer(store):
"""Return a handle that can actually write, asking again when it cannot.

The daemon warms ``local_store.get_store`` up before ``local_server``
marks this process the writer owner, so the handle captured at that
moment can be the read-only proxy the dashboard uses. Collection is
started with that captured handle and refused forever, while every later
caller in the same process is handed the real writer. Resolve it here
instead of trusting the handle the caller happened to hold.
"""
if store is not None and not getattr(store, "_read_only", True):
return store
from clawmetry import local_store

return local_store.get_store(read_only=False)


def start(store):
"""Start local journal ingestion once; replace a previously ended worker.

Expand All @@ -33,7 +50,7 @@ def start(store):
with _lock:
if _worker is not None and _worker.is_alive():
return
reader = AsimovLocalStore(store, initialize=True)
reader = AsimovLocalStore(daemon_writer(store), initialize=True)
_stop.clear()

def collect():
Expand Down
59 changes: 59 additions & 0 deletions tests/test_asimov_daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,65 @@ def fail_once(self, directory, **kwargs):
assert "collector will try again" in caplog.text


def test_start_resolves_the_writer_when_warm_up_captured_the_read_only_proxy(
daemon, writer, monkeypatch):
"""A handle captured before writer ownership must not disable collection.

REQ-ASIMOV-001: free local evidence cannot depend on the daemon happening
to warm its store up after ``local_server`` marked this process the
writer owner. The daemon does it in the other order, so the handle passed
in here can be the dashboard's read-only proxy, and the collector refused
to start on every boot of an install that had already registered a node.
"""
from clawmetry import local_store
from clawmetry.asimov.store import AsimovLocalStore

class ReadOnlyProxy:
_read_only = True

ingested = threading.Event()
ingest = AsimovLocalStore.ingest
asked = []

def observe(self, directory, **kwargs):
result = ingest(self, directory, **kwargs)
ingested.set()
return result

def get_store(read_only=False):
asked.append(read_only)
return writer

monkeypatch.setattr(AsimovLocalStore, "ingest", observe)
monkeypatch.setattr(local_store, "get_store", get_store)
daemon.start(ReadOnlyProxy())
assert ingested.wait(3), "the collector refused a store it could have resolved"
assert daemon._worker.is_alive()
assert asked == [False], "the collector asks once, for the writer"


def test_start_keeps_a_writer_it_was_already_given(daemon, writer, monkeypatch):
"""Resolving must not churn the store for the ordinary, correct call."""
from clawmetry import local_store
from clawmetry.asimov.store import AsimovLocalStore

ingested = threading.Event()
ingest = AsimovLocalStore.ingest

def observe(self, directory, **kwargs):
result = ingest(self, directory, **kwargs)
ingested.set()
return result

def refuse(*args, **kwargs):
raise AssertionError("a usable writer was re-resolved")

monkeypatch.setattr(AsimovLocalStore, "ingest", observe)
monkeypatch.setattr(local_store, "get_store", refuse)
daemon.start(writer)
assert ingested.wait(3)


def test_start_replaces_an_actually_terminated_thread(daemon, writer, monkeypatch):
from clawmetry.asimov.store import AsimovLocalStore

Expand Down
Loading