159 lines
5.1 KiB
Python
159 lines
5.1 KiB
Python
|
|
"""
|
||
|
|
Concurrency / race condition tests.
|
||
|
|
|
||
|
|
Verifies Zinc SHM IPC is safe under concurrent reader/writer access.
|
||
|
|
Uses threading to simulate real multi-process patterns.
|
||
|
|
"""
|
||
|
|
import threading
|
||
|
|
import time
|
||
|
|
import pytest
|
||
|
|
from malkhut.ipc.zinc_plane import MalkhutZincPlane
|
||
|
|
from malkhut.ipc.control_plane import MalkhutControlPlane, ControlPlaneFrame
|
||
|
|
|
||
|
|
|
||
|
|
class TestZincSHMConcurrency:
|
||
|
|
def test_writer_reader_concurrent(self):
|
||
|
|
"""Writer and reader operate concurrently without corruption."""
|
||
|
|
plane = MalkhutZincPlane(prefix="concurrent_test")
|
||
|
|
errors = []
|
||
|
|
|
||
|
|
def writer():
|
||
|
|
try:
|
||
|
|
for i in range(20):
|
||
|
|
plane.publish_book({"seq": i, "ts": time.time_ns()})
|
||
|
|
time.sleep(0.001)
|
||
|
|
except Exception as e:
|
||
|
|
errors.append(("writer", e))
|
||
|
|
|
||
|
|
def reader():
|
||
|
|
try:
|
||
|
|
for _ in range(20):
|
||
|
|
try:
|
||
|
|
data, seq = plane.read_book(timeout_ms=50)
|
||
|
|
except Exception:
|
||
|
|
pass # timeout is acceptable
|
||
|
|
time.sleep(0.002)
|
||
|
|
except Exception as e:
|
||
|
|
errors.append(("reader", e))
|
||
|
|
|
||
|
|
t1 = threading.Thread(target=writer)
|
||
|
|
t2 = threading.Thread(target=reader)
|
||
|
|
t1.start()
|
||
|
|
t2.start()
|
||
|
|
t1.join(timeout=5)
|
||
|
|
t2.join(timeout=5)
|
||
|
|
plane.close_all()
|
||
|
|
assert len(errors) == 0, f"Errors: {errors}"
|
||
|
|
|
||
|
|
def test_multiple_readers(self):
|
||
|
|
"""Multiple readers can read from the same region without conflict."""
|
||
|
|
plane = MalkhutZincPlane(prefix="multi_reader_test")
|
||
|
|
plane.publish_book({"test": "data"})
|
||
|
|
results = []
|
||
|
|
errors = []
|
||
|
|
|
||
|
|
def reader(idx):
|
||
|
|
try:
|
||
|
|
for _ in range(10):
|
||
|
|
try:
|
||
|
|
data, seq = plane.read_book(timeout_ms=50)
|
||
|
|
results.append((idx, seq))
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
time.sleep(0.001)
|
||
|
|
except Exception as e:
|
||
|
|
errors.append((idx, e))
|
||
|
|
|
||
|
|
threads = [threading.Thread(target=reader, args=(i,)) for i in range(3)]
|
||
|
|
for t in threads:
|
||
|
|
t.start()
|
||
|
|
for t in threads:
|
||
|
|
t.join(timeout=5)
|
||
|
|
plane.close_all()
|
||
|
|
assert len(errors) == 0
|
||
|
|
assert len(results) > 0
|
||
|
|
|
||
|
|
def test_rapid_write_read_cycles(self):
|
||
|
|
"""Rapid write/read cycles don't corrupt the region."""
|
||
|
|
plane = MalkhutZincPlane(prefix="rapid_test")
|
||
|
|
for i in range(100):
|
||
|
|
plane.publish_book({"cycle": i})
|
||
|
|
data, seq = plane.read_book(timeout_ms=50)
|
||
|
|
assert data["cycle"] == i
|
||
|
|
plane.close_all()
|
||
|
|
|
||
|
|
|
||
|
|
class TestControlPlaneConcurrency:
|
||
|
|
def test_command_write_read(self):
|
||
|
|
"""Control plane commands can be written and read."""
|
||
|
|
cp = MalkhutControlPlane()
|
||
|
|
frame = ControlPlaneFrame(
|
||
|
|
command="START", ts_ns=time.time_ns(),
|
||
|
|
target_symbols=("BTCUSDT",), source="test",
|
||
|
|
)
|
||
|
|
cp.publish_command(frame)
|
||
|
|
cmd = cp.read_command(timeout_ms=50)
|
||
|
|
assert cmd is not None
|
||
|
|
assert cmd.command == "START"
|
||
|
|
assert cmd.target_symbols == ("BTCUSDT",)
|
||
|
|
cp.close()
|
||
|
|
|
||
|
|
def test_ack_write_read(self):
|
||
|
|
"""ACK frames can be written and read."""
|
||
|
|
cp = MalkhutControlPlane()
|
||
|
|
cp.publish_ack("START", time.time_ns(), "ok", "engine started")
|
||
|
|
cmd = cp.read_command(timeout_ms=50)
|
||
|
|
assert cmd is not None
|
||
|
|
assert "ACK_START" in cmd.command
|
||
|
|
cp.close()
|
||
|
|
|
||
|
|
def test_emergency_stop(self):
|
||
|
|
"""Emergency stop command is processed."""
|
||
|
|
cp = MalkhutControlPlane()
|
||
|
|
frame = ControlPlaneFrame(
|
||
|
|
command="EMERGENCY_STOP", ts_ns=time.time_ns(), source="test",
|
||
|
|
)
|
||
|
|
cp.publish_command(frame)
|
||
|
|
cmd = cp.read_command(timeout_ms=50)
|
||
|
|
assert cmd is not None
|
||
|
|
assert cmd.command == "EMERGENCY_STOP"
|
||
|
|
cp.close()
|
||
|
|
|
||
|
|
|
||
|
|
class TestZincRegionIsolation:
|
||
|
|
def test_different_prefixes_independent(self):
|
||
|
|
"""Different prefixes create independent regions."""
|
||
|
|
p1 = MalkhutZincPlane(prefix="iso_a")
|
||
|
|
p2 = MalkhutZincPlane(prefix="iso_b")
|
||
|
|
|
||
|
|
p1.publish_book({"source": "a"})
|
||
|
|
p2.publish_book({"source": "b"})
|
||
|
|
|
||
|
|
d1, _ = p1.read_book()
|
||
|
|
d2, _ = p2.read_book()
|
||
|
|
assert d1["source"] == "a"
|
||
|
|
assert d2["source"] == "b"
|
||
|
|
|
||
|
|
p1.close_all()
|
||
|
|
p2.close_all()
|
||
|
|
|
||
|
|
def test_multiple_region_types(self):
|
||
|
|
"""Different region types (book, account, fulfilment, risk) are independent."""
|
||
|
|
plane = MalkhutZincPlane(prefix="multi_region")
|
||
|
|
plane.publish_book({"type": "book"})
|
||
|
|
plane.publish_account({"type": "account"})
|
||
|
|
plane.publish_fulfilment({"type": "fulfilment"})
|
||
|
|
plane.publish_risk({"type": "risk"})
|
||
|
|
|
||
|
|
b, _ = plane.read_book()
|
||
|
|
a, _ = plane.read_account()
|
||
|
|
f, _ = plane.read_fulfilment()
|
||
|
|
r, _ = plane.read_risk()
|
||
|
|
|
||
|
|
assert b["type"] == "book"
|
||
|
|
assert a["type"] == "account"
|
||
|
|
assert f["type"] == "fulfilment"
|
||
|
|
assert r["type"] == "risk"
|
||
|
|
|
||
|
|
plane.close_all()
|