Cameras on devices (openpanel C1): frames in memory, a binary uplink, demand
- Volatile frames are held in memory with a byte-bounded queue instead of the /dev/shm ring scanned on every put; workers keep writing to the directory, which is trimmed on a timer. A volatile upload no longer spools to disk. ARTIFACT_VOLATILE_BYTES=0 turns the ring off. - A device sends camera frames as binary messages on its socket, held and published in one step and credited to its device node; refused frames are logged and dropped. Its snapshot carries demand and the uplink (binary, or http through the portal). - One demand registry for every socket's wanted media, the tunnel's included, plus what running flows read, mapped onto on-demand ports and published as device_demand; at most 8 media names per socket. - Per-message deflate off in serve and dev; a device may fetch the last 8 digests of a message; published dashboards cached per store revision; a revoked device socket is closed with 1008. - Media tiles want bytes only while on screen; the Devices tab previews a wired camera; the device node warns when a flow reads an on-demand port.
This commit is contained in:
@@ -141,25 +141,59 @@ def test_a_ring_drops_the_oldest_once_it_is_full(tmp_path):
|
||||
ring = VolatileStore(tmp_path / "ring", limit_bytes=3000)
|
||||
refs = [ring.put([bytes([i]) * 1000], name=f"{i}.bin") for i in range(5)]
|
||||
|
||||
held = [ref for ref in refs if ring.path(ref["digest"]) is not None]
|
||||
held = [ref for ref in refs if ring.has(ref["digest"])]
|
||||
assert len(held) == 3
|
||||
# The newest three, in order: eviction is by age, not by chance.
|
||||
assert held == refs[2:]
|
||||
assert ring.size == 3000
|
||||
# Held in memory: nothing reached the directory.
|
||||
assert not any(ring.root.iterdir())
|
||||
|
||||
|
||||
def test_the_newest_frame_stays_whatever_its_size(tmp_path):
|
||||
"""A frame larger than the bound is still there for its own publish."""
|
||||
ring = VolatileStore(tmp_path / "ring", limit_bytes=100)
|
||||
small = ring.put([b"s" * 50])
|
||||
big = ring.put([b"b" * 500])
|
||||
|
||||
assert ring.get(big["digest"]) == b"b" * 500
|
||||
assert not ring.has(small["digest"])
|
||||
|
||||
|
||||
def test_a_workers_frame_is_read_from_its_directory_once(tmp_path):
|
||||
"""A worker in the container writes its frames to the ring's directory."""
|
||||
import hashlib
|
||||
|
||||
ring = VolatileStore(tmp_path / "ring", limit_bytes=1_000_000)
|
||||
data = b"a worker's frame"
|
||||
body = hashlib.sha256(data).hexdigest()
|
||||
path = ring.root / body[:2] / body
|
||||
path.parent.mkdir(parents=True)
|
||||
path.write_bytes(data)
|
||||
|
||||
assert ring.has("sha256:" + body)
|
||||
assert ring.get("sha256:" + body) == data
|
||||
# Held after the first read, so every screen is served from memory.
|
||||
path.unlink()
|
||||
assert ring.get("sha256:" + body) == data
|
||||
|
||||
|
||||
def test_a_volatile_reference_resolves_like_any_other(tmp_path):
|
||||
"""A frame is an ordinary reference, which is what keeps the rest honest.
|
||||
|
||||
Everything that resolves a digest — serving one, checking a run's input,
|
||||
a panel's scope — goes through ``path``, so the ring has to answer there.
|
||||
Everything that asks whether a digest resolves — checking a run's input,
|
||||
a cached result — goes through ``has``, and reading one through ``read``,
|
||||
so the ring has to answer there.
|
||||
"""
|
||||
store = ArtifactStore(tmp_path / "artifacts")
|
||||
store.volatile = VolatileStore(tmp_path / "ring", limit_bytes=1_000_000)
|
||||
|
||||
ref = store.put([b"frame"], name="f.png", media_type="image/png", volatile=True)
|
||||
|
||||
assert store.path(ref["digest"]) is not None
|
||||
assert store.has(ref["digest"])
|
||||
assert b"".join(store.read(ref["digest"])) == b"frame"
|
||||
# In the ring, and not on the volume the sweep is about.
|
||||
assert store.path(ref["digest"]) is None
|
||||
assert not (store.root / ref["digest"][7:9]).exists()
|
||||
|
||||
|
||||
@@ -171,7 +205,7 @@ def test_a_recorded_frame_is_copied_out_of_the_ring(tmp_path):
|
||||
|
||||
assert store.adopt(ref["digest"]) is True
|
||||
|
||||
store.volatile.collect(set())
|
||||
store.volatile = VolatileStore(tmp_path / "ring2", limit_bytes=1_000_000)
|
||||
assert store.path(ref["digest"]) is not None
|
||||
assert store.adopt("sha256:" + "0" * 64) is False
|
||||
|
||||
@@ -195,7 +229,7 @@ def test_an_upload_may_ask_for_the_ring(
|
||||
assert answer.status_code == 200, answer.text
|
||||
digest = answer.json()["digest"]
|
||||
|
||||
assert store.volatile.path(digest) is not None
|
||||
assert store.volatile.has(digest)
|
||||
assert not (store.root / digest[7:9]).exists()
|
||||
# And it is served back like anything else, which is what the widget
|
||||
# falls back on when the socket did not push it.
|
||||
|
||||
Reference in New Issue
Block a user