fixtureqa 0.4.23__tar.gz → 0.4.24__tar.gz

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.
Files changed (101) hide show
  1. {fixtureqa-0.4.23/fixtureqa.egg-info → fixtureqa-0.4.24}/PKG-INFO +1 -1
  2. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/perf.py +64 -14
  3. {fixtureqa-0.4.23 → fixtureqa-0.4.24/fixtureqa.egg-info}/PKG-INFO +1 -1
  4. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/pyproject.toml +1 -1
  5. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_perf_api.py +46 -5
  6. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/LICENSE +0 -0
  7. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/README.md +0 -0
  8. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/__init__.py +0 -0
  9. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/__main__.py +0 -0
  10. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/__init__.py +0 -0
  11. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/app.py +0 -0
  12. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/connection_manager.py +0 -0
  13. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/deps.py +0 -0
  14. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/__init__.py +0 -0
  15. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/admin.py +0 -0
  16. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/auth.py +0 -0
  17. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/branding.py +0 -0
  18. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/custom_tags.py +0 -0
  19. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/fix_spec.py +0 -0
  20. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/messages.py +0 -0
  21. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/scenarios.py +0 -0
  22. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/sessions.py +0 -0
  23. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/setup.py +0 -0
  24. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/spec_overlay.py +0 -0
  25. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/templates.py +0 -0
  26. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/routers/ws.py +0 -0
  27. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/api/schemas.py +0 -0
  28. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/config/__init__.py +0 -0
  29. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/__init__.py +0 -0
  30. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/atomic_io.py +0 -0
  31. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/auth.py +0 -0
  32. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/config_store.py +0 -0
  33. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/custom_tag_store.py +0 -0
  34. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/db_migrations.py +0 -0
  35. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/events.py +0 -0
  36. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/fix_application.py +0 -0
  37. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/fix_builder.py +0 -0
  38. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/fix_parser.py +0 -0
  39. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/fix_spec_parser.py +0 -0
  40. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/fix_tags.py +0 -0
  41. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/fix_time.py +0 -0
  42. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/housekeeping.py +0 -0
  43. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/inbound.py +0 -0
  44. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/json_store.py +0 -0
  45. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/latency_csv_writer.py +0 -0
  46. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/message_log.py +0 -0
  47. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/message_store.py +0 -0
  48. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/models.py +0 -0
  49. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_charts.py +0 -0
  50. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_engine.py +0 -0
  51. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_models.py +0 -0
  52. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_payload.py +0 -0
  53. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_stats.py +0 -0
  54. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_store.py +0 -0
  55. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/perf_writer.py +0 -0
  56. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/scenario_runner.py +0 -0
  57. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/scenario_store.py +0 -0
  58. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/session.py +0 -0
  59. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/session_manager.py +0 -0
  60. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/spec_overlay_store.py +0 -0
  61. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/template_store.py +0 -0
  62. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/user_store.py +0 -0
  63. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/core/venue_responses.py +0 -0
  64. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/fix_specs/FIX42.xml +0 -0
  65. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/fix_specs/FIX44.xml +0 -0
  66. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/server.py +0 -0
  67. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/assets/ag-grid-_QKprVdm.js +0 -0
  68. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/assets/index-B3ccKxdv.js +0 -0
  69. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/assets/index-CyNOPa0n.js +0 -0
  70. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/assets/index-meqdijDf.css +0 -0
  71. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/assets/react-vendor-2eF0YfZT.js +0 -0
  72. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/favicon.svg +0 -0
  73. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/static/index.html +0 -0
  74. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixture/ui/__init__.py +0 -0
  75. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixtureqa.egg-info/SOURCES.txt +0 -0
  76. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixtureqa.egg-info/dependency_links.txt +0 -0
  77. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixtureqa.egg-info/entry_points.txt +0 -0
  78. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixtureqa.egg-info/requires.txt +0 -0
  79. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/fixtureqa.egg-info/top_level.txt +0 -0
  80. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/setup.cfg +0 -0
  81. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_atomic_io.py +0 -0
  82. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_auth.py +0 -0
  83. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_config_store.py +0 -0
  84. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_connection_manager.py +0 -0
  85. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_db_migrations.py +0 -0
  86. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_fix_builder.py +0 -0
  87. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_health.py +0 -0
  88. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_inbound.py +0 -0
  89. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_inbound_validation.py +0 -0
  90. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_message_store.py +0 -0
  91. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_perf_engine.py +0 -0
  92. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_perf_models.py +0 -0
  93. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_perf_payload.py +0 -0
  94. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_perf_rehydrate.py +0 -0
  95. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_scenarios.py +0 -0
  96. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_session_auto_response.py +0 -0
  97. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_session_lifecycle.py +0 -0
  98. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_session_manager_concurrency.py +0 -0
  99. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_sessions.py +0 -0
  100. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_templates.py +0 -0
  101. {fixtureqa-0.4.23 → fixtureqa-0.4.24}/tests/test_ws.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.23
3
+ Version: 0.4.24
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -44,6 +44,32 @@ Admin = Annotated[User, Depends(require_platform_admin)]
44
44
 
45
45
  _TERMINAL = ("completed", "stopped", "error")
46
46
 
47
+ # Per-message persistence — durable_seqnums (fsync per message) and log_messages
48
+ # (a messages.db write per message) — happens inline on the event loop's
49
+ # send/receive path. Measured sustainable rate is ~36 msg/s on a local disk and
50
+ # materially worse on a Docker/WSL2 named volume; past that the loop stalls on
51
+ # fsync and the whole server appears frozen. We hold a conservative ceiling so a
52
+ # logged run at a low rate still works, while a high-rate logged run is refused
53
+ # up front (before the run starts) rather than wedging the server.
54
+ DURABLE_SAFE_MSG_RATE = 30 # messages/second a logged/durable session can sustain
55
+
56
+
57
+ def _est_msgs_per_order(body: RunConfig) -> int:
58
+ """Rough peak FIX message count generated per order, for the durability rate
59
+ guard: 35=D out + 35=8 ack + fills, plus a request+reply pair for each of the
60
+ amend / cancel flavours when enabled. Deliberately an upper-ish bound."""
61
+ n = 2 + body.payload.fill.fills_per_order # order + ack + fills
62
+ if body.amend.enabled:
63
+ n += 2 # 35=G + 35=8 replace
64
+ if body.cancel.enabled:
65
+ n += 2 # 35=F + 35=8/9 reply
66
+ return max(1, n)
67
+
68
+
69
+ def _order_rate_per_s(body: RunConfig) -> float:
70
+ r = body.rate
71
+ return r.orders_per_window * 1000.0 / r.order_window_ms
72
+
47
73
 
48
74
  def _require_started(sm: SessionManager, sid: str, label: str):
49
75
  try:
@@ -68,10 +94,45 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
68
94
  # (a sync def route runs in a threadpool with no running loop).
69
95
  need_client = body.mode in ("loopback", "manager", "client")
70
96
  need_venue = body.mode in ("loopback", "manager", "venue")
97
+ participating: list[tuple[str, object]] = [] # (label, config) for fixture legs
71
98
  if need_client:
72
- _require_started(sm, body.client_session_id, "client")
99
+ participating.append(("client", _require_started(sm, body.client_session_id, "client").config))
73
100
  if need_venue:
74
- _require_started(sm, body.venue_session_id, "venue")
101
+ participating.append(("venue", _require_started(sm, body.venue_session_id, "venue").config))
102
+
103
+ # Durability rate guard — BEFORE reg.create() (which starts the run). A
104
+ # session with durable_seqnums or log_messages persists every message inline
105
+ # on the event loop; above a low rate this stalls the loop and freezes the
106
+ # whole server. Allow a logged run at a safe low rate (useful to inspect the
107
+ # messages); refuse above it with the safe order rate spelled out.
108
+ logged = [(label, cfg) for label, cfg in participating
109
+ if cfg.durable_seqnums or cfg.log_messages]
110
+ durable_warnings: list[str] = []
111
+ if logged:
112
+ mpo = _est_msgs_per_order(body)
113
+ est_msg_rate = _order_rate_per_s(body) * mpo
114
+ safe_order_rate = DURABLE_SAFE_MSG_RATE / mpo
115
+
116
+ def _flags(cfg) -> str:
117
+ f = [n for n, on in (("durable_seqnums", cfg.durable_seqnums),
118
+ ("log_messages", cfg.log_messages)) if on]
119
+ return "+".join(f)
120
+ who = ", ".join(f"{label} ({_flags(cfg)})" for label, cfg in logged)
121
+
122
+ if est_msg_rate > DURABLE_SAFE_MSG_RATE:
123
+ raise HTTPException(status_code=409, detail=(
124
+ f"This run would generate ~{est_msg_rate:.0f} msg/s "
125
+ f"(~{mpo} msgs/order × {_order_rate_per_s(body):.0f} orders/s), but "
126
+ f"{who} persist every message inline on the event loop "
127
+ f"(fsync / messages.db write). Above ~{DURABLE_SAFE_MSG_RATE} msg/s "
128
+ f"this stalls the server. Either lower the order rate to "
129
+ f"≤ {safe_order_rate:.0f}/s to keep message logging, or convert the "
130
+ f"session(s) to a Performance session (durable_seqnums + log_messages "
131
+ f"off) for high-rate runs."))
132
+ durable_warnings.append(
133
+ f"message persistence is on for {who}; safe only up to "
134
+ f"~{safe_order_rate:.0f} orders/s (~{DURABLE_SAFE_MSG_RATE} msg/s) — at "
135
+ f"higher rates the event loop stalls on per-message fsync/DB writes")
75
136
 
76
137
  # CompIDs from the driving (client, else venue) session — for template tokens.
77
138
  drive_sid = body.client_session_id if need_client else body.venue_session_id
@@ -82,18 +143,7 @@ async def create_run(body: RunConfig, sm: SM, reg: REG, ts: TS, admin: Admin):
82
143
  except PerfConfigError as e:
83
144
  raise HTTPException(status_code=422, detail=str(e))
84
145
 
85
- # Soft perf warnings — don't block, but flag footguns.
86
- checked = []
87
- if need_client:
88
- checked.append((body.client_session_id, "client"))
89
- if need_venue:
90
- checked.append((body.venue_session_id, "venue"))
91
- for sid, label in checked:
92
- cfg = sm.get_state(sid).config
93
- if cfg.durable_seqnums:
94
- run.warnings.append(f"{label} session has durable_seqnums=True — fsync per message caps throughput")
95
- if cfg.log_messages:
96
- run.warnings.append(f"{label} session has log_messages=True — persistence churn adds loop latency")
146
+ run.warnings.extend(durable_warnings)
97
147
  return {"run_id": run.run_id}
98
148
 
99
149
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fixtureqa
3
- Version: 0.4.23
3
+ Version: 0.4.24
4
4
  Summary: FIXture — FIX Protocol Testing Tool
5
5
  Requires-Python: >=3.10
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fixtureqa"
7
- version = "0.4.23"
7
+ version = "0.4.24"
8
8
  description = "FIXture — FIX Protocol Testing Tool"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -15,12 +15,13 @@ def _free_port() -> int:
15
15
  return s.getsockname()[1]
16
16
 
17
17
 
18
- def _perf_session(port, *, acceptor: bool, sender, target, role):
18
+ def _perf_session(port, *, acceptor: bool, sender, target, role,
19
+ durable=False, log=False):
19
20
  body = {
20
21
  "sender_comp_id": sender, "target_comp_id": target,
21
22
  "connection_type": "acceptor" if acceptor else "initiator",
22
23
  "port": port, "use_file_log": False,
23
- "durable_seqnums": False, "log_messages": False, "session_role": role,
24
+ "durable_seqnums": durable, "log_messages": log, "session_role": role,
24
25
  }
25
26
  if not acceptor:
26
27
  body["host"] = "127.0.0.1"
@@ -38,12 +39,14 @@ def _wait_status(client, sid, target, timeout=10.0):
38
39
  raise TimeoutError(f"{sid} did not reach {target} (last {st})")
39
40
 
40
41
 
41
- def _loopback_pair(client):
42
+ def _loopback_pair(client, *, durable=False, log=False):
42
43
  port = _free_port()
43
44
  acc = client.post("/api/sessions/", json=_perf_session(
44
- port, acceptor=True, sender="VENUE", target="CLIENT", role="venue")).json()["session_id"]
45
+ port, acceptor=True, sender="VENUE", target="CLIENT", role="venue",
46
+ durable=durable, log=log)).json()["session_id"]
45
47
  ini = client.post("/api/sessions/", json=_perf_session(
46
- port, acceptor=False, sender="CLIENT", target="VENUE", role="client")).json()["session_id"]
48
+ port, acceptor=False, sender="CLIENT", target="VENUE", role="client",
49
+ durable=durable, log=log)).json()["session_id"]
47
50
  client.post(f"/api/sessions/{acc}/start")
48
51
  client.post(f"/api/sessions/{ini}/start")
49
52
  _wait_status(client, ini, "LOGGED_ON")
@@ -101,6 +104,44 @@ def test_perf_run_lifecycle_and_ws(authed):
101
104
  assert exp.status_code == 200 and exp.text.startswith("id,run_id,")
102
105
 
103
106
 
107
+ def test_perf_run_refused_on_durable_session_at_high_rate(authed):
108
+ # A durable/logged session persists every message inline on the event loop;
109
+ # a high-rate run would stall the server, so it must be refused up front
110
+ # (before the run starts) — not silently freeze.
111
+ client, _ = authed
112
+ ini, acc = _loopback_pair(client, durable=True, log=True)
113
+ # _run_body default: 20 orders/100ms = 200 orders/s → far over the safe rate.
114
+ r = client.post("/api/perf/runs", json=_run_body(ini, acc, duration=2))
115
+ assert r.status_code == 409, r.text
116
+ detail = r.json()["detail"]
117
+ assert "Performance session" in detail and "msg/s" in detail
118
+
119
+
120
+ def test_perf_run_allows_durable_session_at_low_rate(authed):
121
+ # Logging a perf run at a low rate is allowed (useful to inspect messages) —
122
+ # it just carries a warning about the safe ceiling.
123
+ client, _ = authed
124
+ ini, acc = _loopback_pair(client, durable=True, log=True)
125
+ body = _run_body(ini, acc, duration=2)
126
+ body["rate"] = {"orders_per_window": 1, "order_window_ms": 1000,
127
+ "fills_per_window": 1, "fill_window_ms": 1000} # 1 order/s
128
+ r = client.post("/api/perf/runs", json=body)
129
+ assert r.status_code == 201, r.text
130
+ run_id = r.json()["run_id"]
131
+ st = client.get(f"/api/perf/runs/{run_id}").json()
132
+ assert any("message persistence is on" in w for w in st["warnings"]), st["warnings"]
133
+ client.delete(f"/api/perf/runs/{run_id}") # stop the slow run
134
+
135
+
136
+ def test_perf_run_no_rate_limit_on_perf_sessions(authed):
137
+ # Performance sessions (durability off) carry no rate guard — high rate ok.
138
+ client, _ = authed
139
+ ini, acc = _loopback_pair(client) # durable=False, log=False
140
+ r = client.post("/api/perf/runs", json=_run_body(ini, acc, duration=2))
141
+ assert r.status_code == 201, r.text
142
+ client.delete(f"/api/perf/runs/{r.json()['run_id']}")
143
+
144
+
104
145
  def test_perf_configs_crud(authed):
105
146
  client, _ = authed
106
147
  r = client.post("/api/perf/configs", json=_run_body("c", "v"))
File without changes
File without changes
File without changes
File without changes
File without changes