Resolve main merge and add unmanaged store race tests
Co-authored-by: genichin <4282227+genichin@users.noreply.github.com>
This commit is contained in:
co-authored by
genichin
parent
c73e382b70
commit
84d81658a1
@@ -1,6 +1,7 @@
|
|||||||
"""PostgreSQLUnmanagedPositionStore integration tests."""
|
"""PostgreSQLUnmanagedPositionStore integration tests."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -136,3 +137,70 @@ class TestCRUDRoundTrip:
|
|||||||
assert reopened["state"]["last_seen_at"] == t3
|
assert reopened["state"]["last_seen_at"] == t3
|
||||||
assert reopened["state"]["operator_action"] is None
|
assert reopened["state"]["operator_action"] is None
|
||||||
assert reopened["state"]["evidence_ref"] is None
|
assert reopened["state"]["evidence_ref"] is None
|
||||||
|
|
||||||
|
def test_concurrent_first_observe_keeps_single_detected_audit(self, pg_clean):
|
||||||
|
from trex_core.adapters.portfolio.unmanaged_position_store import (
|
||||||
|
PostgreSQLUnmanagedPositionStore,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _observe():
|
||||||
|
return PostgreSQLUnmanagedPositionStore(pg_clean).observe(
|
||||||
|
instance_key="inst-A",
|
||||||
|
symbol="008110",
|
||||||
|
quantity=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||||
|
results = list(pool.map(lambda _n: _observe(), range(2)))
|
||||||
|
|
||||||
|
assert sorted(result["transition"] for result in results) == ["detected", "unchanged"]
|
||||||
|
store = PostgreSQLUnmanagedPositionStore(pg_clean)
|
||||||
|
state = store.get_state(instance_key="inst-A", symbol="008110")
|
||||||
|
assert state is not None
|
||||||
|
assert state["lifecycle_state"] == "active"
|
||||||
|
audit = store.list_audit(instance_key="inst-A", symbol="008110")
|
||||||
|
assert [row["event_type"] for row in audit] == ["detected"]
|
||||||
|
|
||||||
|
def test_concurrent_observe_and_resolve_leave_consistent_state_and_audit(self, pg_clean):
|
||||||
|
from trex_core.adapters.portfolio.unmanaged_position_store import (
|
||||||
|
PostgreSQLUnmanagedPositionStore,
|
||||||
|
)
|
||||||
|
|
||||||
|
store = PostgreSQLUnmanagedPositionStore(pg_clean)
|
||||||
|
store.observe(instance_key="inst-A", symbol="008110", quantity=3)
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
||||||
|
observe_future = pool.submit(
|
||||||
|
PostgreSQLUnmanagedPositionStore(pg_clean).observe,
|
||||||
|
instance_key="inst-A",
|
||||||
|
symbol="008110",
|
||||||
|
quantity=5,
|
||||||
|
)
|
||||||
|
resolve_future = pool.submit(
|
||||||
|
PostgreSQLUnmanagedPositionStore(pg_clean).resolve_absent,
|
||||||
|
instance_key="inst-A",
|
||||||
|
observed_symbols=set(),
|
||||||
|
)
|
||||||
|
observe_result = observe_future.result()
|
||||||
|
resolve_result = resolve_future.result()
|
||||||
|
|
||||||
|
state = store.get_state(instance_key="inst-A", symbol="008110")
|
||||||
|
assert state is not None
|
||||||
|
audit = store.list_audit(instance_key="inst-A", symbol="008110")
|
||||||
|
event_types = [row["event_type"] for row in audit]
|
||||||
|
assert event_types[0] == "detected"
|
||||||
|
assert event_types in (
|
||||||
|
["detected", "quantity_changed", "resolved"],
|
||||||
|
["detected", "resolved", "detected"],
|
||||||
|
)
|
||||||
|
if state["lifecycle_state"] == "resolved":
|
||||||
|
assert observe_result["transition"] == "quantity_changed"
|
||||||
|
assert len(resolve_result) == 1
|
||||||
|
assert state["quantity"] == 5
|
||||||
|
assert event_types[-1] == "resolved"
|
||||||
|
else:
|
||||||
|
assert state["lifecycle_state"] == "active"
|
||||||
|
assert observe_result["transition"] == "detected"
|
||||||
|
assert len(resolve_result) == 1
|
||||||
|
assert state["quantity"] == 5
|
||||||
|
assert event_types[-1] == "detected"
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ def test_in_memory_store_tracks_lifecycle_and_operator_audit():
|
|||||||
"operator_action",
|
"operator_action",
|
||||||
"resolved",
|
"resolved",
|
||||||
]
|
]
|
||||||
|
assert all("recorded_at" in row for row in audit)
|
||||||
|
|
||||||
|
|
||||||
def test_in_memory_store_isolates_same_symbol_by_instance():
|
def test_in_memory_store_isolates_same_symbol_by_instance():
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ class PostgreSQLUnmanagedPositionStore:
|
|||||||
"actor": row[4],
|
"actor": row[4],
|
||||||
"evidence_ref": row[5],
|
"evidence_ref": row[5],
|
||||||
"note": row[6],
|
"note": row[6],
|
||||||
"created_at": row[7],
|
"recorded_at": row[7],
|
||||||
}
|
}
|
||||||
for row in cur.fetchall()
|
for row in cur.fetchall()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -660,7 +660,7 @@ class InMemoryUnmanagedPositionStore:
|
|||||||
"operator_note": "",
|
"operator_note": "",
|
||||||
"operator_actor": None,
|
"operator_actor": None,
|
||||||
"operator_updated_at": None,
|
"operator_updated_at": None,
|
||||||
"created_at": now,
|
"recorded_at": now,
|
||||||
**preserved,
|
**preserved,
|
||||||
"updated_at": now,
|
"updated_at": now,
|
||||||
}
|
}
|
||||||
@@ -695,7 +695,7 @@ class InMemoryUnmanagedPositionStore:
|
|||||||
if transition == "quantity_changed" and previous_quantity is not None
|
if transition == "quantity_changed" and previous_quantity is not None
|
||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
"created_at": now,
|
"recorded_at": now,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -737,7 +737,7 @@ class InMemoryUnmanagedPositionStore:
|
|||||||
"actor": actor,
|
"actor": actor,
|
||||||
"evidence_ref": state.get("evidence_ref"),
|
"evidence_ref": state.get("evidence_ref"),
|
||||||
"note": "",
|
"note": "",
|
||||||
"created_at": now,
|
"recorded_at": now,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return resolved
|
return resolved
|
||||||
@@ -798,7 +798,7 @@ class InMemoryUnmanagedPositionStore:
|
|||||||
"actor": actor,
|
"actor": actor,
|
||||||
"evidence_ref": evidence_ref,
|
"evidence_ref": evidence_ref,
|
||||||
"note": note,
|
"note": note,
|
||||||
"created_at": now,
|
"recorded_at": now,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return dict(state)
|
return dict(state)
|
||||||
|
|||||||
Reference in New Issue
Block a user