cfquant 0.2.0__py3-none-any.whl
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.
- LTtx/__init__.py +2 -0
- LTtx/tx/Config.txt +8 -0
- LTtx/tx/LTtx_server.py +1177 -0
- LTtx/tx/__init__.py +5 -0
- LTtx/tx/tx.py +1935 -0
- cfquant/__init__.py +26 -0
- cfquant/account_routing.py +119 -0
- cfquant/channels.py +76 -0
- cfquant/cli.py +266 -0
- cfquant/client.py +365 -0
- cfquant/config.py +86 -0
- cfquant/hub.py +304 -0
- cfquant/logging_i18n.py +348 -0
- cfquant/normal_bridge.py +676 -0
- cfquant/pipe_bridge.py +249 -0
- cfquant/pipe_client.py +223 -0
- cfquant/pipe_hub.py +602 -0
- cfquant/pipe_transport.py +493 -0
- cfquant/protocol.py +238 -0
- cfquant/qmt_bridge.py +1081 -0
- cfquant/runtime_report.py +334 -0
- cfquant/tx.py +1898 -0
- cfquant/tx_trade_bridge.py +1905 -0
- cfquant/version.py +5 -0
- cfquant/xtconstant.py +262 -0
- cfquant/xtdata.py +462 -0
- cfquant/xttrader.py +955 -0
- cfquant/xttype.py +586 -0
- cfquant-0.2.0.dist-info/LICENSE +21 -0
- cfquant-0.2.0.dist-info/METADATA +144 -0
- cfquant-0.2.0.dist-info/RECORD +90 -0
- cfquant-0.2.0.dist-info/WHEEL +5 -0
- cfquant-0.2.0.dist-info/entry_points.txt +4 -0
- cfquant-0.2.0.dist-info/top_level.txt +6 -0
- cfquant_pipe_hub.py +45 -0
- cfquant_web_server.py +12137 -0
- qmt_scripts/CFQUANT.py +507 -0
- qmt_scripts/CFQUANT_CTYPE_ALL_LOWLAT.py +830 -0
- qmt_scripts/CFQUANT_LITE.py +4928 -0
- qmt_scripts/CFQUANT_TRADE_LOWLAT.py +382 -0
- qmt_scripts/__init__.py +1 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SH.py +866 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_CTYPE_ALL_LOWLAT_SZ.py +866 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SH.py +5020 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_LITE_SZ.py +5020 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SH.py +436 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/CFQUANT_TRADE_LOWLAT_SZ.py +436 -0
- qmt_scripts//321/205/320/240/320/234/321/210/342/224/244/320/266/321/205/320/237/342/225/226/321/207/320/233/320/274/321/207/320/273/320/233/321/205/342/225/225/320/222/321/205/320/254/342/225/221/readme.md +95 -0
- web_dashboard/__init__.py +1 -0
- web_dashboard/app.js +9847 -0
- web_dashboard/avatars/amber-pulse.svg +15 -0
- web_dashboard/avatars/copper-grid.svg +15 -0
- web_dashboard/avatars/market-blue.svg +15 -0
- web_dashboard/avatars/rose-circuit.svg +15 -0
- web_dashboard/avatars/signal-green.svg +15 -0
- web_dashboard/avatars/slate-wave.svg +15 -0
- web_dashboard/avatars/teal-orbit.svg +16 -0
- web_dashboard/avatars/violet-node.svg +15 -0
- web_dashboard/favicon.svg +5 -0
- web_dashboard/image/qmt_deploy/image-10.png +0 -0
- web_dashboard/image/qmt_deploy/image-11.png +0 -0
- web_dashboard/image/qmt_deploy/image-12.png +0 -0
- web_dashboard/image/qmt_deploy/image-13.png +0 -0
- web_dashboard/image/qmt_deploy/image-14.png +0 -0
- web_dashboard/image/qmt_deploy/image-15.png +0 -0
- web_dashboard/image/qmt_deploy/image-16.png +0 -0
- web_dashboard/image/qmt_deploy/image-17.png +0 -0
- web_dashboard/image/qmt_deploy/image-18.png +0 -0
- web_dashboard/image/qmt_deploy/image-19.png +0 -0
- web_dashboard/image/qmt_deploy/image-20.png +0 -0
- web_dashboard/image/qmt_deploy/image-21.png +0 -0
- web_dashboard/image/qmt_deploy/image-4.png +0 -0
- web_dashboard/image/qmt_deploy/image-5.png +0 -0
- web_dashboard/image/qmt_deploy/image-6.png +0 -0
- web_dashboard/image/qmt_deploy/image-7.png +0 -0
- web_dashboard/image/qmt_deploy/image-8.png +0 -0
- web_dashboard/image/qmt_deploy/image-9.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-1.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-2.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image-3.png +0 -0
- web_dashboard/image//321/211/320/220/320/252/321/207/320/244/320/270/321/206/320/270/320/261/321/205/342/225/235/320/237/321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/image.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479325179.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783479745262.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480178700.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783480428759.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783489807217.png +0 -0
- web_dashboard/image//321/211/320/223/320/270/321/207/342/225/234/342/226/223/321/206/320/234/320/227/321/205/320/235/320/247/1783490503491.png +0 -0
- web_dashboard/index.html +2317 -0
- web_dashboard/styles.css +7817 -0
- web_dashboard/vendor/mermaid.min.js +2029 -0
cfquant/pipe_hub.py
ADDED
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
from .pipe_transport import (
|
|
8
|
+
DEFAULT_PIPE_NAME,
|
|
9
|
+
create_pipe_instance,
|
|
10
|
+
dumps_pipe_message,
|
|
11
|
+
loads_pipe_message,
|
|
12
|
+
normalize_pipe_name,
|
|
13
|
+
wait_for_pipe_client,
|
|
14
|
+
)
|
|
15
|
+
from .protocol import loads_message, pack_event, pack_response
|
|
16
|
+
from .version import __version__ as CORE_VERSION
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def default_status_file(filename):
|
|
20
|
+
runtime_dir = os.path.abspath(os.environ.get("CFQUANT_RUNTIME_DIR") or os.path.join(os.getcwd(), "runtime"))
|
|
21
|
+
return os.path.join(runtime_dir, "status", filename)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CfquantPipeHub(object):
|
|
25
|
+
"""
|
|
26
|
+
External named-pipe hub.
|
|
27
|
+
|
|
28
|
+
QMT-side pipe bridges register as role=qmt with a request_channel.
|
|
29
|
+
External API clients register as role=api and send cfquant request payloads.
|
|
30
|
+
The hub forwards requests to the matching QMT bridge and routes responses or
|
|
31
|
+
events back to the originating API client.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, pipe_name=None, show=True, default_request_channel="cfquant.normal.request"):
|
|
35
|
+
self.pipe_name = normalize_pipe_name(pipe_name or DEFAULT_PIPE_NAME)
|
|
36
|
+
self.show = show
|
|
37
|
+
self.default_request_channel = default_request_channel
|
|
38
|
+
self.running = False
|
|
39
|
+
self.listener = None
|
|
40
|
+
self.verbose_events = str(os.environ.get("CFQUANT_PIPE_HUB_VERBOSE_EVENTS") or "").strip().lower() in (
|
|
41
|
+
"1",
|
|
42
|
+
"true",
|
|
43
|
+
"yes",
|
|
44
|
+
"on",
|
|
45
|
+
)
|
|
46
|
+
self.qmt_rx_by_channel = {}
|
|
47
|
+
self.qmt_tx_by_channel = {}
|
|
48
|
+
self.qmt_channel_by_conn = {}
|
|
49
|
+
self.qmt_lock = threading.RLock()
|
|
50
|
+
self.pending = {}
|
|
51
|
+
self.client_by_id = {}
|
|
52
|
+
self.client_tx_by_id = {}
|
|
53
|
+
self.client_ids_by_conn = {}
|
|
54
|
+
self.client_conn_meta_by_conn = {}
|
|
55
|
+
self.client_generation_by_id = {}
|
|
56
|
+
self.state_lock = threading.RLock()
|
|
57
|
+
self.status_file = os.path.abspath(
|
|
58
|
+
os.environ.get("CFQUANT_PIPE_HUB_STATUS_FILE") or default_status_file("cfquant_pipe_hub_status.json")
|
|
59
|
+
)
|
|
60
|
+
self.pending_timeout_seconds = float(os.environ.get("CFQUANT_PIPE_HUB_PENDING_TIMEOUT", "60"))
|
|
61
|
+
self.maintenance_interval_seconds = float(os.environ.get("CFQUANT_PIPE_HUB_MAINTENANCE_INTERVAL", "2"))
|
|
62
|
+
self.maintenance_thread = None
|
|
63
|
+
|
|
64
|
+
def start(self):
|
|
65
|
+
if self.running:
|
|
66
|
+
return self
|
|
67
|
+
self.running = True
|
|
68
|
+
self._log("pipe hub started pipe=%s" % self.pipe_name)
|
|
69
|
+
self._start_maintenance()
|
|
70
|
+
self._write_status()
|
|
71
|
+
while self.running:
|
|
72
|
+
conn = None
|
|
73
|
+
try:
|
|
74
|
+
conn = create_pipe_instance(self.pipe_name)
|
|
75
|
+
self.listener = conn
|
|
76
|
+
wait_for_pipe_client(conn)
|
|
77
|
+
self._log("pipe client connected")
|
|
78
|
+
thread = threading.Thread(target=self._client_loop, args=(conn,))
|
|
79
|
+
thread.daemon = True
|
|
80
|
+
thread.start()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
if conn is not None:
|
|
83
|
+
try:
|
|
84
|
+
conn.close()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
if self.running:
|
|
88
|
+
self._log("pipe accept failed: %s" % e)
|
|
89
|
+
time.sleep(0.2)
|
|
90
|
+
return self
|
|
91
|
+
|
|
92
|
+
def close(self):
|
|
93
|
+
self.running = False
|
|
94
|
+
try:
|
|
95
|
+
if self.listener is not None:
|
|
96
|
+
self.listener.close()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
with self.qmt_lock:
|
|
100
|
+
conns = list(self.qmt_rx_by_channel.values()) + list(self.qmt_tx_by_channel.values())
|
|
101
|
+
self.qmt_rx_by_channel.clear()
|
|
102
|
+
self.qmt_tx_by_channel.clear()
|
|
103
|
+
self.qmt_channel_by_conn.clear()
|
|
104
|
+
with self.state_lock:
|
|
105
|
+
conns.extend(self.client_ids_by_conn.keys())
|
|
106
|
+
self.pending.clear()
|
|
107
|
+
self.client_by_id.clear()
|
|
108
|
+
self.client_tx_by_id.clear()
|
|
109
|
+
self.client_ids_by_conn.clear()
|
|
110
|
+
self.client_conn_meta_by_conn.clear()
|
|
111
|
+
self.client_generation_by_id.clear()
|
|
112
|
+
for conn in conns:
|
|
113
|
+
try:
|
|
114
|
+
conn.close()
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
self._write_status()
|
|
118
|
+
|
|
119
|
+
def _client_loop(self, conn):
|
|
120
|
+
role = "api"
|
|
121
|
+
try:
|
|
122
|
+
while self.running:
|
|
123
|
+
raw = conn.read_frame()
|
|
124
|
+
if raw is None:
|
|
125
|
+
break
|
|
126
|
+
envelope = loads_pipe_message(raw)
|
|
127
|
+
if envelope:
|
|
128
|
+
msg_type = envelope.get("type")
|
|
129
|
+
if msg_type == "hello":
|
|
130
|
+
role = self._handle_hello(conn, envelope, role)
|
|
131
|
+
if role in ("api_rx", "qmt_rx"):
|
|
132
|
+
self._passive_rx_loop(conn, role)
|
|
133
|
+
break
|
|
134
|
+
elif msg_type == "request":
|
|
135
|
+
role = "api"
|
|
136
|
+
self._handle_api_request(conn, envelope)
|
|
137
|
+
elif msg_type == "publish":
|
|
138
|
+
role = "qmt"
|
|
139
|
+
self._handle_qmt_publish(conn, envelope)
|
|
140
|
+
else:
|
|
141
|
+
self._log("pipe ignored envelope type=%s role=%s" % (msg_type, role))
|
|
142
|
+
else:
|
|
143
|
+
self._handle_api_request(conn, {
|
|
144
|
+
"payload": raw,
|
|
145
|
+
"request_channel": self.default_request_channel,
|
|
146
|
+
})
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if self.running:
|
|
149
|
+
self._log("pipe client loop failed role=%s error=%s" % (role, e))
|
|
150
|
+
finally:
|
|
151
|
+
self._drop_conn(conn)
|
|
152
|
+
try:
|
|
153
|
+
conn.close()
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
self._log("pipe client disconnected role=%s" % role)
|
|
157
|
+
self._write_status()
|
|
158
|
+
|
|
159
|
+
def _passive_rx_loop(self, conn, role):
|
|
160
|
+
while self.running:
|
|
161
|
+
if role == "qmt_rx":
|
|
162
|
+
with self.qmt_lock:
|
|
163
|
+
active = conn in self.qmt_channel_by_conn
|
|
164
|
+
elif role == "api_rx":
|
|
165
|
+
with self.state_lock:
|
|
166
|
+
active = conn in self.client_ids_by_conn
|
|
167
|
+
else:
|
|
168
|
+
active = False
|
|
169
|
+
if not active:
|
|
170
|
+
break
|
|
171
|
+
time.sleep(0.2)
|
|
172
|
+
|
|
173
|
+
def _handle_hello(self, conn, envelope, current_role):
|
|
174
|
+
role = envelope.get("role") or current_role
|
|
175
|
+
if role in ("qmt", "qmt_rx", "qmt_tx"):
|
|
176
|
+
channels = self._envelope_channels(envelope)
|
|
177
|
+
with self.qmt_lock:
|
|
178
|
+
target = self.qmt_rx_by_channel if role in ("qmt", "qmt_rx") else self.qmt_tx_by_channel
|
|
179
|
+
for channel in channels:
|
|
180
|
+
old = target.get(channel)
|
|
181
|
+
target[channel] = conn
|
|
182
|
+
self.qmt_channel_by_conn.setdefault(conn, set()).add(channel)
|
|
183
|
+
if old is not None and old is not conn:
|
|
184
|
+
try:
|
|
185
|
+
old.close()
|
|
186
|
+
except Exception:
|
|
187
|
+
pass
|
|
188
|
+
self._log(
|
|
189
|
+
"qmt pipe bridge registered role=%s channels=%s bridge_id=%s"
|
|
190
|
+
% (role, ",".join(channels), envelope.get("bridge_id") or "-")
|
|
191
|
+
)
|
|
192
|
+
elif role in ("api", "api_rx", "api_tx"):
|
|
193
|
+
client_id = envelope.get("client_id")
|
|
194
|
+
if client_id:
|
|
195
|
+
self._remember_client(conn, client_id, receive_conn=role in ("api", "api_rx"))
|
|
196
|
+
self._log("api pipe client registered role=%s client_id=%s" % (role, client_id or "-"))
|
|
197
|
+
self._write_status()
|
|
198
|
+
return role
|
|
199
|
+
|
|
200
|
+
def _envelope_channels(self, envelope):
|
|
201
|
+
raw = envelope.get("request_channels")
|
|
202
|
+
if raw is None:
|
|
203
|
+
raw = [envelope.get("request_channel") or self.default_request_channel]
|
|
204
|
+
result = []
|
|
205
|
+
for channel in raw:
|
|
206
|
+
channel = str(channel or "").strip()
|
|
207
|
+
if channel and channel not in result:
|
|
208
|
+
result.append(channel)
|
|
209
|
+
return result or [self.default_request_channel]
|
|
210
|
+
|
|
211
|
+
def _handle_api_request(self, conn, envelope):
|
|
212
|
+
api_received_at = time.perf_counter()
|
|
213
|
+
raw = envelope.get("payload")
|
|
214
|
+
msg = loads_message(raw)
|
|
215
|
+
if not msg or msg.get("type") != "request":
|
|
216
|
+
return
|
|
217
|
+
request_id = msg.get("id")
|
|
218
|
+
client_id = envelope.get("client_id") or msg.get("client_id") or msg.get("reply_channel")
|
|
219
|
+
request_channel = envelope.get("request_channel") or self.default_request_channel
|
|
220
|
+
if client_id:
|
|
221
|
+
self._remember_client(conn, client_id, receive_conn=False)
|
|
222
|
+
response_conn = self._client_rx_conn(client_id) or conn
|
|
223
|
+
with self.state_lock:
|
|
224
|
+
if request_id:
|
|
225
|
+
self.pending[request_id] = {
|
|
226
|
+
"conn": response_conn,
|
|
227
|
+
"qmt_conn": None,
|
|
228
|
+
"action": msg.get("action"),
|
|
229
|
+
"request_channel": request_channel,
|
|
230
|
+
"api_received_at": api_received_at,
|
|
231
|
+
"forward_done_at": None,
|
|
232
|
+
}
|
|
233
|
+
qmt = self._qmt_rx_conn(request_channel)
|
|
234
|
+
if qmt is None:
|
|
235
|
+
with self.state_lock:
|
|
236
|
+
self.pending.pop(request_id, None)
|
|
237
|
+
self._send_error(response_conn, request_id, "QMT pipe bridge not connected for channel=%s" % request_channel)
|
|
238
|
+
return
|
|
239
|
+
with self.state_lock:
|
|
240
|
+
pending = self.pending.get(request_id)
|
|
241
|
+
if pending:
|
|
242
|
+
pending["qmt_conn"] = qmt
|
|
243
|
+
forward_start = time.perf_counter()
|
|
244
|
+
try:
|
|
245
|
+
self._send_delivery(qmt, raw, request_channel=request_channel, client_id=client_id)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self._drop_conn(qmt)
|
|
248
|
+
with self.state_lock:
|
|
249
|
+
self.pending.pop(request_id, None)
|
|
250
|
+
self._send_error(
|
|
251
|
+
response_conn,
|
|
252
|
+
request_id,
|
|
253
|
+
"QMT pipe bridge send failed for channel=%s error=%s" % (request_channel, e),
|
|
254
|
+
)
|
|
255
|
+
return
|
|
256
|
+
forward_done = time.perf_counter()
|
|
257
|
+
with self.state_lock:
|
|
258
|
+
pending = self.pending.get(request_id)
|
|
259
|
+
if pending:
|
|
260
|
+
pending["forward_done_at"] = forward_done
|
|
261
|
+
self._log(
|
|
262
|
+
"pipe forwarded request action=%s id=%s channel=%s api_to_forward_ms=%.2f send_ms=%.2f len=%s"
|
|
263
|
+
% (
|
|
264
|
+
msg.get("action"),
|
|
265
|
+
request_id,
|
|
266
|
+
request_channel,
|
|
267
|
+
self._elapsed_ms(api_received_at, forward_done),
|
|
268
|
+
self._elapsed_ms(forward_start, forward_done),
|
|
269
|
+
len(raw or ""),
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
self._write_status()
|
|
273
|
+
|
|
274
|
+
def _handle_qmt_publish(self, conn, envelope):
|
|
275
|
+
qmt_received_at = time.perf_counter()
|
|
276
|
+
raw = envelope.get("payload")
|
|
277
|
+
msg = loads_message(raw)
|
|
278
|
+
target = None
|
|
279
|
+
status_changed = False
|
|
280
|
+
if not msg:
|
|
281
|
+
client_id = envelope.get("channel") or envelope.get("client_id")
|
|
282
|
+
if client_id:
|
|
283
|
+
with self.state_lock:
|
|
284
|
+
target = self.client_by_id.get(client_id)
|
|
285
|
+
if target is not None:
|
|
286
|
+
try:
|
|
287
|
+
callback_event = self._pack_callback_event(raw)
|
|
288
|
+
if callback_event is not None:
|
|
289
|
+
self._send_delivery(target, callback_event)
|
|
290
|
+
if self.verbose_events:
|
|
291
|
+
self._log(
|
|
292
|
+
"pipe got raw qmt callback channel=%s target=%s len=%s"
|
|
293
|
+
% (client_id, bool(target), len(raw or ""))
|
|
294
|
+
)
|
|
295
|
+
return
|
|
296
|
+
except Exception as e:
|
|
297
|
+
self._log("pipe raw qmt callback pack failed: %s" % e)
|
|
298
|
+
return
|
|
299
|
+
msg_type = msg.get("type")
|
|
300
|
+
if msg_type == "response":
|
|
301
|
+
request_id = msg.get("id")
|
|
302
|
+
with self.state_lock:
|
|
303
|
+
pending = self.pending.pop(request_id, None)
|
|
304
|
+
status_changed = pending is not None
|
|
305
|
+
if pending:
|
|
306
|
+
target = pending.get("conn")
|
|
307
|
+
api_received_at = pending.get("api_received_at") or qmt_received_at
|
|
308
|
+
forward_done_at = pending.get("forward_done_at") or api_received_at
|
|
309
|
+
self._log(
|
|
310
|
+
"pipe got qmt response action=%s id=%s channel=%s qmt_roundtrip_ms=%.2f total_to_hub_ms=%.2f len=%s"
|
|
311
|
+
% (
|
|
312
|
+
pending.get("action"),
|
|
313
|
+
request_id,
|
|
314
|
+
pending.get("request_channel"),
|
|
315
|
+
self._elapsed_ms(forward_done_at, qmt_received_at),
|
|
316
|
+
self._elapsed_ms(api_received_at, qmt_received_at),
|
|
317
|
+
len(raw or ""),
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
elif msg_type == "event":
|
|
321
|
+
client_id = msg.get("client_id") or envelope.get("channel")
|
|
322
|
+
with self.state_lock:
|
|
323
|
+
target = self.client_by_id.get(client_id)
|
|
324
|
+
if self.verbose_events:
|
|
325
|
+
self._log(
|
|
326
|
+
"pipe got qmt event event=%s client_id=%s target=%s len=%s"
|
|
327
|
+
% (msg.get("event"), client_id, bool(target), len(raw or ""))
|
|
328
|
+
)
|
|
329
|
+
if target is not None:
|
|
330
|
+
try:
|
|
331
|
+
self._send_delivery(target, raw)
|
|
332
|
+
except Exception as e:
|
|
333
|
+
self._log("pipe response/event delivery failed: %s" % e)
|
|
334
|
+
self._drop_conn(target)
|
|
335
|
+
if status_changed:
|
|
336
|
+
self._write_status()
|
|
337
|
+
|
|
338
|
+
def _pack_callback_event(self, raw):
|
|
339
|
+
try:
|
|
340
|
+
payload = json.loads(raw)
|
|
341
|
+
except Exception:
|
|
342
|
+
payload = {"raw": raw}
|
|
343
|
+
if not isinstance(payload, dict):
|
|
344
|
+
payload = {"raw": payload}
|
|
345
|
+
event_name = str(payload.get("event") or payload.get("event_name") or "callback")
|
|
346
|
+
client_id = str(payload.get("client_id") or payload.get("account_id") or payload.get("bridge_id") or "")
|
|
347
|
+
return pack_event(
|
|
348
|
+
event_name,
|
|
349
|
+
data=payload,
|
|
350
|
+
client_id=client_id or None,
|
|
351
|
+
subscription_id=payload.get("subscription_id") or payload.get("subscribe_id"),
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def _send_error(self, conn, request_id, message):
|
|
355
|
+
try:
|
|
356
|
+
self._send_delivery(
|
|
357
|
+
conn,
|
|
358
|
+
pack_response(request_id, ok=False, error={"type": "ConnectionError", "message": message}),
|
|
359
|
+
)
|
|
360
|
+
except Exception as e:
|
|
361
|
+
self._log("pipe error delivery failed: %s" % e)
|
|
362
|
+
self._drop_conn(conn)
|
|
363
|
+
|
|
364
|
+
def _send_delivery(self, conn, payload, request_channel=None, client_id=None):
|
|
365
|
+
conn.write_frame(dumps_pipe_message({
|
|
366
|
+
"type": "delivery",
|
|
367
|
+
"payload": payload,
|
|
368
|
+
"request_channel": request_channel,
|
|
369
|
+
"client_id": client_id,
|
|
370
|
+
}))
|
|
371
|
+
|
|
372
|
+
def _qmt_rx_conn(self, request_channel):
|
|
373
|
+
with self.qmt_lock:
|
|
374
|
+
return self.qmt_rx_by_channel.get(request_channel)
|
|
375
|
+
|
|
376
|
+
def _client_rx_conn(self, client_id):
|
|
377
|
+
if not client_id:
|
|
378
|
+
return None
|
|
379
|
+
with self.state_lock:
|
|
380
|
+
return self.client_by_id.get(client_id)
|
|
381
|
+
|
|
382
|
+
def _remember_client(self, conn, client_id, receive_conn=True):
|
|
383
|
+
close_old = []
|
|
384
|
+
with self.state_lock:
|
|
385
|
+
if receive_conn:
|
|
386
|
+
generation = int(self.client_generation_by_id.get(client_id) or 0) + 1
|
|
387
|
+
self.client_generation_by_id[client_id] = generation
|
|
388
|
+
mapping = self.client_by_id
|
|
389
|
+
else:
|
|
390
|
+
generation = int(self.client_generation_by_id.get(client_id) or 0) or 1
|
|
391
|
+
self.client_generation_by_id[client_id] = generation
|
|
392
|
+
mapping = self.client_tx_by_id
|
|
393
|
+
old = mapping.get(client_id)
|
|
394
|
+
if old is not None and old is not conn:
|
|
395
|
+
self._detach_client_conn_locked(old, client_id)
|
|
396
|
+
close_old.append(old)
|
|
397
|
+
if receive_conn:
|
|
398
|
+
self.client_by_id[client_id] = conn
|
|
399
|
+
else:
|
|
400
|
+
self.client_tx_by_id[client_id] = conn
|
|
401
|
+
self.client_ids_by_conn.setdefault(conn, set()).add(client_id)
|
|
402
|
+
self.client_conn_meta_by_conn.setdefault(conn, {})[client_id] = {
|
|
403
|
+
"receive": bool(receive_conn),
|
|
404
|
+
"generation": generation,
|
|
405
|
+
}
|
|
406
|
+
for old in close_old:
|
|
407
|
+
try:
|
|
408
|
+
old.close()
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
def _drop_conn(self, conn):
|
|
413
|
+
failed_pending = []
|
|
414
|
+
with self.qmt_lock:
|
|
415
|
+
channels = self.qmt_channel_by_conn.pop(conn, set())
|
|
416
|
+
for channel in channels:
|
|
417
|
+
if self.qmt_rx_by_channel.get(channel) is conn:
|
|
418
|
+
self.qmt_rx_by_channel.pop(channel, None)
|
|
419
|
+
if self.qmt_tx_by_channel.get(channel) is conn:
|
|
420
|
+
self.qmt_tx_by_channel.pop(channel, None)
|
|
421
|
+
close_peers = []
|
|
422
|
+
with self.state_lock:
|
|
423
|
+
meta_by_client = self.client_conn_meta_by_conn.pop(conn, {})
|
|
424
|
+
client_ids = set(self.client_ids_by_conn.pop(conn, set())) | set(meta_by_client.keys())
|
|
425
|
+
for client_id in client_ids:
|
|
426
|
+
meta = meta_by_client.get(client_id) or {}
|
|
427
|
+
generation = meta.get("generation")
|
|
428
|
+
receive_conn = meta.get("receive")
|
|
429
|
+
rx_conn = self.client_by_id.get(client_id)
|
|
430
|
+
tx_conn = self.client_tx_by_id.get(client_id)
|
|
431
|
+
rx_generation = self._client_conn_generation_locked(rx_conn, client_id)
|
|
432
|
+
tx_generation = self._client_conn_generation_locked(tx_conn, client_id)
|
|
433
|
+
|
|
434
|
+
if rx_conn is conn:
|
|
435
|
+
self.client_by_id.pop(client_id, None)
|
|
436
|
+
if tx_conn is conn:
|
|
437
|
+
self.client_tx_by_id.pop(client_id, None)
|
|
438
|
+
|
|
439
|
+
if receive_conn is True:
|
|
440
|
+
peer = tx_conn
|
|
441
|
+
peer_generation = tx_generation
|
|
442
|
+
peer_mapping = self.client_tx_by_id
|
|
443
|
+
elif receive_conn is False:
|
|
444
|
+
peer = rx_conn
|
|
445
|
+
peer_generation = rx_generation
|
|
446
|
+
peer_mapping = self.client_by_id
|
|
447
|
+
else:
|
|
448
|
+
peer = None
|
|
449
|
+
peer_generation = None
|
|
450
|
+
peer_mapping = None
|
|
451
|
+
|
|
452
|
+
if (
|
|
453
|
+
peer is not None
|
|
454
|
+
and peer is not conn
|
|
455
|
+
and generation is not None
|
|
456
|
+
and peer_generation == generation
|
|
457
|
+
and peer not in close_peers
|
|
458
|
+
):
|
|
459
|
+
close_peers.append(peer)
|
|
460
|
+
if peer_mapping is not None and peer_mapping.get(client_id) is peer:
|
|
461
|
+
peer_mapping.pop(client_id, None)
|
|
462
|
+
self._detach_client_conn_locked(peer, client_id)
|
|
463
|
+
|
|
464
|
+
if self.client_by_id.get(client_id) is None and self.client_tx_by_id.get(client_id) is None:
|
|
465
|
+
self.client_generation_by_id.pop(client_id, None)
|
|
466
|
+
for request_id, pending in list(self.pending.items()):
|
|
467
|
+
pending_conn = pending.get("conn")
|
|
468
|
+
qmt_conn = pending.get("qmt_conn")
|
|
469
|
+
if pending_conn is conn or pending_conn in close_peers:
|
|
470
|
+
self.pending.pop(request_id, None)
|
|
471
|
+
elif qmt_conn is conn:
|
|
472
|
+
self.pending.pop(request_id, None)
|
|
473
|
+
failed_pending.append((
|
|
474
|
+
request_id,
|
|
475
|
+
pending,
|
|
476
|
+
"QMT pipe bridge disconnected for channel=%s" % pending.get("request_channel"),
|
|
477
|
+
))
|
|
478
|
+
for peer in close_peers:
|
|
479
|
+
try:
|
|
480
|
+
peer.close()
|
|
481
|
+
except Exception:
|
|
482
|
+
pass
|
|
483
|
+
for request_id, pending, message in failed_pending:
|
|
484
|
+
self._send_error(pending.get("conn"), request_id, message)
|
|
485
|
+
|
|
486
|
+
def _detach_client_conn_locked(self, conn, client_id):
|
|
487
|
+
if conn is None:
|
|
488
|
+
return
|
|
489
|
+
client_ids = self.client_ids_by_conn.get(conn)
|
|
490
|
+
if client_ids is not None:
|
|
491
|
+
client_ids.discard(client_id)
|
|
492
|
+
if not client_ids:
|
|
493
|
+
self.client_ids_by_conn.pop(conn, None)
|
|
494
|
+
meta_by_client = self.client_conn_meta_by_conn.get(conn)
|
|
495
|
+
if meta_by_client is not None:
|
|
496
|
+
meta_by_client.pop(client_id, None)
|
|
497
|
+
if not meta_by_client:
|
|
498
|
+
self.client_conn_meta_by_conn.pop(conn, None)
|
|
499
|
+
|
|
500
|
+
def _client_conn_generation_locked(self, conn, client_id):
|
|
501
|
+
if conn is None:
|
|
502
|
+
return None
|
|
503
|
+
meta_by_client = self.client_conn_meta_by_conn.get(conn) or {}
|
|
504
|
+
meta = meta_by_client.get(client_id) or {}
|
|
505
|
+
return meta.get("generation")
|
|
506
|
+
|
|
507
|
+
def _start_maintenance(self):
|
|
508
|
+
if self.maintenance_thread is not None and self.maintenance_thread.is_alive():
|
|
509
|
+
return
|
|
510
|
+
self.maintenance_thread = threading.Thread(target=self._maintenance_loop)
|
|
511
|
+
self.maintenance_thread.daemon = True
|
|
512
|
+
self.maintenance_thread.start()
|
|
513
|
+
|
|
514
|
+
def _maintenance_loop(self):
|
|
515
|
+
while self.running:
|
|
516
|
+
try:
|
|
517
|
+
expired_count = self._cleanup_expired_pending()
|
|
518
|
+
if expired_count:
|
|
519
|
+
self._write_status()
|
|
520
|
+
except Exception as e:
|
|
521
|
+
self._log("pipe hub maintenance failed: %s" % e)
|
|
522
|
+
time.sleep(max(0.2, self.maintenance_interval_seconds))
|
|
523
|
+
|
|
524
|
+
def _cleanup_expired_pending(self):
|
|
525
|
+
timeout = max(0.0, self.pending_timeout_seconds)
|
|
526
|
+
if timeout <= 0:
|
|
527
|
+
return 0
|
|
528
|
+
now = time.perf_counter()
|
|
529
|
+
expired = []
|
|
530
|
+
with self.state_lock:
|
|
531
|
+
for request_id, pending in list(self.pending.items()):
|
|
532
|
+
started = pending.get("api_received_at") or now
|
|
533
|
+
if now - started < timeout:
|
|
534
|
+
continue
|
|
535
|
+
self.pending.pop(request_id, None)
|
|
536
|
+
expired.append((request_id, pending))
|
|
537
|
+
for request_id, pending in expired:
|
|
538
|
+
self._send_error(
|
|
539
|
+
pending.get("conn"),
|
|
540
|
+
request_id,
|
|
541
|
+
"QMT pipe bridge response timeout for action=%s channel=%s"
|
|
542
|
+
% (pending.get("action"), pending.get("request_channel")),
|
|
543
|
+
)
|
|
544
|
+
if expired:
|
|
545
|
+
self._log("pipe cleaned expired pending requests count=%s" % len(expired))
|
|
546
|
+
return len(expired)
|
|
547
|
+
|
|
548
|
+
def _log(self, msg):
|
|
549
|
+
if self.show:
|
|
550
|
+
print("%s %s" % (self._timestamp_ms(), msg), flush=True)
|
|
551
|
+
|
|
552
|
+
def _timestamp_ms(self):
|
|
553
|
+
now = time.time()
|
|
554
|
+
local = time.localtime(now)
|
|
555
|
+
return "%s.%03d" % (time.strftime("%Y-%m-%d %H:%M:%S", local), int((now - int(now)) * 1000))
|
|
556
|
+
|
|
557
|
+
def _elapsed_ms(self, start, end=None):
|
|
558
|
+
if end is None:
|
|
559
|
+
end = time.perf_counter()
|
|
560
|
+
return (end - start) * 1000.0
|
|
561
|
+
|
|
562
|
+
def _write_status(self):
|
|
563
|
+
try:
|
|
564
|
+
with self.qmt_lock:
|
|
565
|
+
qmt_rx_channels = sorted(set(self.qmt_rx_by_channel.keys()))
|
|
566
|
+
qmt_tx_channels = sorted(set(self.qmt_tx_by_channel.keys()))
|
|
567
|
+
qmt_channels = sorted(set(qmt_rx_channels) | set(qmt_tx_channels))
|
|
568
|
+
qmt_ready_channels = sorted(set(qmt_rx_channels) & set(qmt_tx_channels))
|
|
569
|
+
with self.state_lock:
|
|
570
|
+
pending_ids = list(self.pending.keys())[-20:]
|
|
571
|
+
client_count = len(set(self.client_by_id.values()))
|
|
572
|
+
data = {
|
|
573
|
+
"ts": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
574
|
+
"pipe_name": self.pipe_name,
|
|
575
|
+
"core_version": CORE_VERSION,
|
|
576
|
+
"hub_status_schema": 2,
|
|
577
|
+
"running": self.running,
|
|
578
|
+
"pid": os.getpid(),
|
|
579
|
+
"qmt_channels": qmt_channels,
|
|
580
|
+
"qmt_rx_channels": qmt_rx_channels,
|
|
581
|
+
"qmt_tx_channels": qmt_tx_channels,
|
|
582
|
+
"qmt_ready_channels": qmt_ready_channels,
|
|
583
|
+
"qmt_connected": bool(qmt_ready_channels or qmt_channels),
|
|
584
|
+
"pending_count": len(self.pending),
|
|
585
|
+
"pending_ids": pending_ids,
|
|
586
|
+
"api_client_count": client_count,
|
|
587
|
+
}
|
|
588
|
+
status_dir = os.path.dirname(self.status_file)
|
|
589
|
+
if status_dir:
|
|
590
|
+
os.makedirs(status_dir, exist_ok=True)
|
|
591
|
+
with open(self.status_file, "w", encoding="utf-8") as f:
|
|
592
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def run_pipe_hub(pipe_name=None, show=True, default_request_channel="cfquant.normal.request"):
|
|
598
|
+
return CfquantPipeHub(
|
|
599
|
+
pipe_name=pipe_name,
|
|
600
|
+
show=show,
|
|
601
|
+
default_request_channel=default_request_channel,
|
|
602
|
+
).start()
|