ubdcc 0.3.1__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.
- ubdcc/__init__.py +20 -0
- ubdcc/__main__.py +3 -0
- ubdcc/cli.py +534 -0
- ubdcc-0.3.1.dist-info/METADATA +137 -0
- ubdcc-0.3.1.dist-info/RECORD +9 -0
- ubdcc-0.3.1.dist-info/WHEEL +5 -0
- ubdcc-0.3.1.dist-info/entry_points.txt +2 -0
- ubdcc-0.3.1.dist-info/licenses/LICENSE +21 -0
- ubdcc-0.3.1.dist-info/top_level.txt +1 -0
ubdcc/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ¯\_(ツ)_/¯
|
|
4
|
+
#
|
|
5
|
+
# File: packages/ubdcc/ubdcc/__init__.py
|
|
6
|
+
#
|
|
7
|
+
# Project website: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
|
|
8
|
+
# Github: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
|
|
9
|
+
# Documentation: https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster
|
|
10
|
+
# PyPI: https://pypi.org/project/ubdcc
|
|
11
|
+
#
|
|
12
|
+
# License: MIT
|
|
13
|
+
# https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/LICENSE
|
|
14
|
+
#
|
|
15
|
+
# Author: Oliver Zehentleitner
|
|
16
|
+
#
|
|
17
|
+
# Copyright (c) 2024-2026, Oliver Zehentleitner (https://about.me/oliver-zehentleitner)
|
|
18
|
+
# All rights reserved.
|
|
19
|
+
|
|
20
|
+
__version__ = "0.2.0"
|
ubdcc/__main__.py
ADDED
ubdcc/cli.py
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ¯\_(ツ)_/¯
|
|
4
|
+
#
|
|
5
|
+
# File: packages/ubdcc/ubdcc/cli.py
|
|
6
|
+
#
|
|
7
|
+
# Project website: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
|
|
8
|
+
# Github: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
|
|
9
|
+
# Documentation: https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster
|
|
10
|
+
# PyPI: https://pypi.org/project/ubdcc
|
|
11
|
+
#
|
|
12
|
+
# License: MIT
|
|
13
|
+
# https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/LICENSE
|
|
14
|
+
#
|
|
15
|
+
# Author: Oliver Zehentleitner
|
|
16
|
+
#
|
|
17
|
+
# Copyright (c) 2024-2026, Oliver Zehentleitner (https://about.me/oliver-zehentleitner)
|
|
18
|
+
# All rights reserved.
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import signal
|
|
23
|
+
import socket
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
|
|
28
|
+
import requests
|
|
29
|
+
|
|
30
|
+
from ubdcc import __version__
|
|
31
|
+
|
|
32
|
+
STATE_FILE = ".ubdcc"
|
|
33
|
+
DEFAULT_MGMT_PORT = 42080
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def save_port(port):
|
|
37
|
+
with open(STATE_FILE, "w") as f:
|
|
38
|
+
f.write(str(port))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_port():
|
|
42
|
+
try:
|
|
43
|
+
with open(STATE_FILE, "r") as f:
|
|
44
|
+
return int(f.read().strip())
|
|
45
|
+
except (FileNotFoundError, ValueError):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_mgmt_port(args):
|
|
50
|
+
"""Resolve mgmt port: --port flag > state file > default."""
|
|
51
|
+
if args.port is not None:
|
|
52
|
+
return args.port
|
|
53
|
+
saved = load_port()
|
|
54
|
+
if saved is not None:
|
|
55
|
+
return saved
|
|
56
|
+
return DEFAULT_MGMT_PORT
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_port_free(port, host='127.0.0.1'):
|
|
60
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
61
|
+
try:
|
|
62
|
+
s.bind((host, port))
|
|
63
|
+
return True
|
|
64
|
+
except OSError:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def find_free_port(start=42080):
|
|
69
|
+
port = start
|
|
70
|
+
while not is_port_free(port):
|
|
71
|
+
port += 1
|
|
72
|
+
return port
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def wait_for_cluster(mgmt_port, expected_pods, timeout=120):
|
|
76
|
+
"""Poll get_cluster_info until all expected pods are registered."""
|
|
77
|
+
url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
|
|
78
|
+
start_time = time.time()
|
|
79
|
+
while time.time() - start_time < timeout:
|
|
80
|
+
try:
|
|
81
|
+
response = requests.get(url, timeout=5)
|
|
82
|
+
data = response.json()
|
|
83
|
+
if data.get('result') == 'OK' and data.get('db', {}).get('pods'):
|
|
84
|
+
registered = len(data['db']['pods'])
|
|
85
|
+
if registered >= expected_pods:
|
|
86
|
+
return data
|
|
87
|
+
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
|
|
88
|
+
pass
|
|
89
|
+
time.sleep(1)
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def cmd_start(args):
|
|
94
|
+
mgmt_port = args.port if args.port else find_free_port(DEFAULT_MGMT_PORT)
|
|
95
|
+
dcn_count = args.dcn
|
|
96
|
+
logdir = args.logdir if args.logdir else os.getcwd()
|
|
97
|
+
os.makedirs(logdir, exist_ok=True)
|
|
98
|
+
save_port(mgmt_port)
|
|
99
|
+
|
|
100
|
+
print(f"UBDCC Cluster Manager v{__version__}")
|
|
101
|
+
print(f"Starting cluster with mgmt port {mgmt_port}, {dcn_count} DCN(s)...")
|
|
102
|
+
print(f"Log directory: {logdir}")
|
|
103
|
+
|
|
104
|
+
processes = []
|
|
105
|
+
cwd = os.getcwd()
|
|
106
|
+
dcn_counter = [0] # mutable for closure access
|
|
107
|
+
|
|
108
|
+
def spawn_mgmt():
|
|
109
|
+
log = open(os.path.join(logdir, "ubdcc-mgmt.log"), "a")
|
|
110
|
+
proc = subprocess.Popen(
|
|
111
|
+
[sys.executable, "-c",
|
|
112
|
+
f"import os; from ubdcc_mgmt.Mgmt import Mgmt; Mgmt(cwd='{cwd}', mgmt_port={mgmt_port})"],
|
|
113
|
+
stdout=log, stderr=subprocess.STDOUT
|
|
114
|
+
)
|
|
115
|
+
# Remove old mgmt from processes list
|
|
116
|
+
for i, (name, p, l) in enumerate(processes):
|
|
117
|
+
if name == "mgmt":
|
|
118
|
+
processes.pop(i)
|
|
119
|
+
break
|
|
120
|
+
processes.append(("mgmt", proc, log))
|
|
121
|
+
return proc.pid
|
|
122
|
+
|
|
123
|
+
def spawn_restapi():
|
|
124
|
+
log = open(os.path.join(logdir, "ubdcc-restapi.log"), "a")
|
|
125
|
+
proc = subprocess.Popen(
|
|
126
|
+
[sys.executable, "-c",
|
|
127
|
+
f"import os; from ubdcc_restapi.RestApi import RestApi; RestApi(cwd='{cwd}', mgmt_port={mgmt_port})"],
|
|
128
|
+
stdout=log, stderr=subprocess.STDOUT
|
|
129
|
+
)
|
|
130
|
+
# Remove old restapi from processes list
|
|
131
|
+
for i, (name, p, l) in enumerate(processes):
|
|
132
|
+
if name == "restapi":
|
|
133
|
+
processes.pop(i)
|
|
134
|
+
break
|
|
135
|
+
processes.append(("restapi", proc, log))
|
|
136
|
+
return proc.pid
|
|
137
|
+
|
|
138
|
+
def spawn_dcn():
|
|
139
|
+
dcn_counter[0] += 1
|
|
140
|
+
nr = dcn_counter[0]
|
|
141
|
+
log = open(os.path.join(logdir, f"ubdcc-dcn-{nr}.log"), "a")
|
|
142
|
+
proc = subprocess.Popen(
|
|
143
|
+
[sys.executable, "-c",
|
|
144
|
+
f"import os; from ubdcc_dcn.DepthCacheNode import DepthCacheNode; "
|
|
145
|
+
f"DepthCacheNode(cwd='{cwd}', mgmt_port={mgmt_port})"],
|
|
146
|
+
stdout=log, stderr=subprocess.STDOUT
|
|
147
|
+
)
|
|
148
|
+
processes.append((f"dcn-{nr}", proc, log))
|
|
149
|
+
return nr, proc.pid
|
|
150
|
+
|
|
151
|
+
# Start mgmt
|
|
152
|
+
pid = spawn_mgmt()
|
|
153
|
+
print(f" mgmt started (PID {pid})")
|
|
154
|
+
|
|
155
|
+
# Start restapi
|
|
156
|
+
pid = spawn_restapi()
|
|
157
|
+
print(f" restapi started (PID {pid})")
|
|
158
|
+
|
|
159
|
+
# Start DCNs
|
|
160
|
+
for i in range(dcn_count):
|
|
161
|
+
nr, pid = spawn_dcn()
|
|
162
|
+
print(f" dcn-{nr} started (PID {pid})")
|
|
163
|
+
|
|
164
|
+
expected_pods = 1 + dcn_count # restapi + DCNs (mgmt doesn't register itself)
|
|
165
|
+
print(f"\nWaiting for {expected_pods} pods to register with mgmt...")
|
|
166
|
+
|
|
167
|
+
cluster_info = wait_for_cluster(mgmt_port, expected_pods)
|
|
168
|
+
if cluster_info:
|
|
169
|
+
print(f"Cluster is ready!\n")
|
|
170
|
+
print_status_table(cluster_info, mgmt_port=mgmt_port)
|
|
171
|
+
else:
|
|
172
|
+
print("Warning: Timeout waiting for all pods to register. Check the logs.")
|
|
173
|
+
|
|
174
|
+
print(f"\nType 'help' for available commands, Ctrl+C or 'stop' to shut down.\n")
|
|
175
|
+
|
|
176
|
+
# Interactive console
|
|
177
|
+
def do_shutdown():
|
|
178
|
+
print("\nShutting down cluster...")
|
|
179
|
+
shutdown_all(mgmt_port)
|
|
180
|
+
for name, proc, log in processes:
|
|
181
|
+
proc.terminate()
|
|
182
|
+
log.close()
|
|
183
|
+
try:
|
|
184
|
+
os.remove(STATE_FILE)
|
|
185
|
+
except FileNotFoundError:
|
|
186
|
+
pass
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
|
|
189
|
+
signal.signal(signal.SIGINT, lambda sig, frame: do_shutdown())
|
|
190
|
+
signal.signal(signal.SIGTERM, lambda sig, frame: do_shutdown())
|
|
191
|
+
|
|
192
|
+
while True:
|
|
193
|
+
try:
|
|
194
|
+
raw = input("ubdcc> ").strip()
|
|
195
|
+
except (KeyboardInterrupt, EOFError):
|
|
196
|
+
do_shutdown()
|
|
197
|
+
|
|
198
|
+
if not raw:
|
|
199
|
+
continue
|
|
200
|
+
parts = raw.split(None, 1)
|
|
201
|
+
cmd = parts[0].lower()
|
|
202
|
+
arg = parts[1] if len(parts) > 1 else None
|
|
203
|
+
|
|
204
|
+
if cmd in ('status', '/status'):
|
|
205
|
+
try:
|
|
206
|
+
response = requests.get(f"http://127.0.0.1:{mgmt_port}/get_cluster_info", timeout=5)
|
|
207
|
+
data = response.json()
|
|
208
|
+
if data.get('result') == 'OK':
|
|
209
|
+
print()
|
|
210
|
+
print_status_table(data, mgmt_port=mgmt_port)
|
|
211
|
+
print()
|
|
212
|
+
except requests.exceptions.ConnectionError:
|
|
213
|
+
print("Cannot connect to mgmt.")
|
|
214
|
+
elif cmd in ('stop', '/stop', 'quit', 'exit'):
|
|
215
|
+
do_shutdown()
|
|
216
|
+
elif cmd in ('add-dcn', '/add-dcn'):
|
|
217
|
+
count = int(arg) if arg else 1
|
|
218
|
+
for _ in range(count):
|
|
219
|
+
nr, pid = spawn_dcn()
|
|
220
|
+
print(f" dcn-{nr} started (PID {pid})")
|
|
221
|
+
print(f"Waiting for registration...")
|
|
222
|
+
time.sleep(3)
|
|
223
|
+
elif cmd in ('remove-dcn', '/remove-dcn'):
|
|
224
|
+
if not arg:
|
|
225
|
+
print("Usage: remove-dcn <count|pod-name>")
|
|
226
|
+
else:
|
|
227
|
+
try:
|
|
228
|
+
count = int(arg)
|
|
229
|
+
remove_dcn_by_count(mgmt_port, count, processes)
|
|
230
|
+
except ValueError:
|
|
231
|
+
remove_dcn(mgmt_port, arg, processes)
|
|
232
|
+
elif cmd in ('restart', '/restart'):
|
|
233
|
+
if not arg:
|
|
234
|
+
print("Usage: restart <pod-name|mgmt|restapi>")
|
|
235
|
+
elif arg in ('ubdcc-mgmt', 'mgmt'):
|
|
236
|
+
try:
|
|
237
|
+
requests.get(f"http://127.0.0.1:{mgmt_port}/shutdown", timeout=5)
|
|
238
|
+
except requests.exceptions.ConnectionError:
|
|
239
|
+
pass
|
|
240
|
+
for name, proc, log in processes:
|
|
241
|
+
if name == "mgmt":
|
|
242
|
+
proc.wait(timeout=10)
|
|
243
|
+
break
|
|
244
|
+
pid = spawn_mgmt()
|
|
245
|
+
print(f" mgmt restarted (PID {pid})")
|
|
246
|
+
print(" Waiting for mgmt to come up...")
|
|
247
|
+
for _ in range(30):
|
|
248
|
+
try:
|
|
249
|
+
requests.get(f"http://127.0.0.1:{mgmt_port}/test", timeout=2)
|
|
250
|
+
print(" mgmt is ready!")
|
|
251
|
+
break
|
|
252
|
+
except requests.exceptions.ConnectionError:
|
|
253
|
+
time.sleep(1)
|
|
254
|
+
elif arg in ('ubdcc-restapi', 'restapi'):
|
|
255
|
+
try:
|
|
256
|
+
data = requests.get(f"http://127.0.0.1:{mgmt_port}/get_cluster_info", timeout=5).json()
|
|
257
|
+
for uid, pod in data.get('db', {}).get('pods', {}).items():
|
|
258
|
+
if pod.get('ROLE') == 'ubdcc-restapi':
|
|
259
|
+
requests.get(f"http://127.0.0.1:{pod['API_PORT_REST']}/shutdown", timeout=3)
|
|
260
|
+
break
|
|
261
|
+
except requests.exceptions.ConnectionError:
|
|
262
|
+
pass
|
|
263
|
+
for name, proc, log in processes:
|
|
264
|
+
if name == "restapi":
|
|
265
|
+
proc.wait(timeout=10)
|
|
266
|
+
break
|
|
267
|
+
pid = spawn_restapi()
|
|
268
|
+
print(f" restapi restarted (PID {pid})")
|
|
269
|
+
else:
|
|
270
|
+
restart_pod(mgmt_port, arg)
|
|
271
|
+
nr, pid = spawn_dcn()
|
|
272
|
+
print(f" dcn-{nr} respawned (PID {pid})")
|
|
273
|
+
elif cmd in ('help', '/help', '?'):
|
|
274
|
+
print()
|
|
275
|
+
print("Available commands:")
|
|
276
|
+
print(" status Show cluster status")
|
|
277
|
+
print(" add-dcn [count] Spawn new DCN process(es)")
|
|
278
|
+
print(" remove-dcn <count|name> Stop and remove DCN(s)")
|
|
279
|
+
print(" restart <name> Restart a specific pod")
|
|
280
|
+
print(" stop Shut down the cluster")
|
|
281
|
+
print(" help Show this help")
|
|
282
|
+
print()
|
|
283
|
+
else:
|
|
284
|
+
print(f"Unknown command: {cmd}. Type 'help' for available commands.")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def cmd_status(args):
|
|
288
|
+
mgmt_port = get_mgmt_port(args)
|
|
289
|
+
url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
|
|
290
|
+
try:
|
|
291
|
+
response = requests.get(url, timeout=5)
|
|
292
|
+
data = response.json()
|
|
293
|
+
if data.get('result') == 'OK':
|
|
294
|
+
print_status_table(data, mgmt_port=mgmt_port)
|
|
295
|
+
else:
|
|
296
|
+
print(f"Error: {data.get('message', 'Unknown error')}")
|
|
297
|
+
except requests.exceptions.ConnectionError:
|
|
298
|
+
print(f"Cannot connect to mgmt on port {mgmt_port}. Is the cluster running?")
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def cmd_stop(args):
|
|
302
|
+
mgmt_port = get_mgmt_port(args)
|
|
303
|
+
shutdown_all(mgmt_port)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def cmd_restart(args):
|
|
307
|
+
mgmt_port = get_mgmt_port(args)
|
|
308
|
+
restart_pod(mgmt_port, args.name)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def remove_dcn_by_count(mgmt_port, count, processes):
|
|
312
|
+
"""Stop N DCN pods."""
|
|
313
|
+
url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
|
|
314
|
+
try:
|
|
315
|
+
response = requests.get(url, timeout=5)
|
|
316
|
+
data = response.json()
|
|
317
|
+
except requests.exceptions.ConnectionError:
|
|
318
|
+
print(f"Cannot connect to mgmt on port {mgmt_port}.")
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
pods = data.get('db', {}).get('pods', {})
|
|
322
|
+
dcns = [(uid, pod) for uid, pod in pods.items() if pod.get('ROLE') == 'ubdcc-dcn']
|
|
323
|
+
if count > len(dcns):
|
|
324
|
+
print(f"Only {len(dcns)} DCN(s) running, cannot remove {count}.")
|
|
325
|
+
return
|
|
326
|
+
|
|
327
|
+
removed = 0
|
|
328
|
+
for uid, pod in dcns:
|
|
329
|
+
if removed >= count:
|
|
330
|
+
break
|
|
331
|
+
port = pod['API_PORT_REST']
|
|
332
|
+
name = pod['NAME']
|
|
333
|
+
try:
|
|
334
|
+
requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=5)
|
|
335
|
+
print(f" Removed: {name} (port {port})")
|
|
336
|
+
removed += 1
|
|
337
|
+
except requests.exceptions.ConnectionError:
|
|
338
|
+
print(f" Warning: Could not reach {name} on port {port}")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def remove_dcn(mgmt_port, target, processes):
|
|
342
|
+
"""Stop a DCN pod and remove it from the process list."""
|
|
343
|
+
url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
|
|
344
|
+
try:
|
|
345
|
+
response = requests.get(url, timeout=5)
|
|
346
|
+
data = response.json()
|
|
347
|
+
except requests.exceptions.ConnectionError:
|
|
348
|
+
print(f"Cannot connect to mgmt on port {mgmt_port}.")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
pods = data.get('db', {}).get('pods', {})
|
|
352
|
+
for uid, pod in pods.items():
|
|
353
|
+
if pod['NAME'] == target or uid == target:
|
|
354
|
+
if pod.get('ROLE') != 'ubdcc-dcn':
|
|
355
|
+
print(f"'{target}' is not a DCN. Use 'remove-dcn' only for DCN pods.")
|
|
356
|
+
return
|
|
357
|
+
port = pod['API_PORT_REST']
|
|
358
|
+
name = pod['NAME']
|
|
359
|
+
try:
|
|
360
|
+
requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=5)
|
|
361
|
+
print(f" Removed: {name} (port {port})")
|
|
362
|
+
except requests.exceptions.ConnectionError:
|
|
363
|
+
print(f" Warning: Could not reach {name} on port {port}")
|
|
364
|
+
# Remove from local process list
|
|
365
|
+
for i, (pname, proc, log) in enumerate(processes):
|
|
366
|
+
if proc.poll() is not None or pname.startswith('dcn'):
|
|
367
|
+
# Can't match by name since local names differ from pod names
|
|
368
|
+
# Just clean up dead processes
|
|
369
|
+
pass
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
print(f"Pod '{target}' not found. Use 'status' to see available pods.")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def restart_pod(mgmt_port, target):
|
|
376
|
+
"""Send shutdown to a specific pod by name or UID."""
|
|
377
|
+
# Handle mgmt separately — it doesn't register itself in the pod list
|
|
378
|
+
if target in ('ubdcc-mgmt', 'mgmt'):
|
|
379
|
+
try:
|
|
380
|
+
requests.get(f"http://127.0.0.1:{mgmt_port}/shutdown", timeout=5)
|
|
381
|
+
print(f"Shutdown signal sent to mgmt on port {mgmt_port}.")
|
|
382
|
+
print(f"Note: Restart the process manually or re-run 'ubdcc start'.")
|
|
383
|
+
except requests.exceptions.ConnectionError:
|
|
384
|
+
print(f"Cannot connect to mgmt on port {mgmt_port}.")
|
|
385
|
+
return
|
|
386
|
+
|
|
387
|
+
url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
|
|
388
|
+
try:
|
|
389
|
+
response = requests.get(url, timeout=5)
|
|
390
|
+
data = response.json()
|
|
391
|
+
except requests.exceptions.ConnectionError:
|
|
392
|
+
print(f"Cannot connect to mgmt on port {mgmt_port}. Is the cluster running?")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
pods = data.get('db', {}).get('pods', {})
|
|
396
|
+
for uid, pod in pods.items():
|
|
397
|
+
if pod['NAME'] == target or uid == target:
|
|
398
|
+
port = pod['API_PORT_REST']
|
|
399
|
+
name = pod['NAME']
|
|
400
|
+
try:
|
|
401
|
+
requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=5)
|
|
402
|
+
print(f"Shutdown signal sent to '{name}' on port {port}.")
|
|
403
|
+
print(f"Note: Restart the process manually or re-run 'ubdcc start'.")
|
|
404
|
+
except requests.exceptions.ConnectionError:
|
|
405
|
+
print(f"Cannot connect to '{name}' on port {port}.")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
print(f"Pod '{target}' not found. Use 'status' to see available pods.")
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def shutdown_all(mgmt_port):
|
|
412
|
+
"""Shutdown all pods via their /shutdown endpoints."""
|
|
413
|
+
url = f"http://127.0.0.1:{mgmt_port}/get_cluster_info"
|
|
414
|
+
try:
|
|
415
|
+
response = requests.get(url, timeout=5)
|
|
416
|
+
data = response.json()
|
|
417
|
+
except requests.exceptions.ConnectionError:
|
|
418
|
+
print(f"Cannot connect to mgmt on port {mgmt_port}.")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
pods = data.get('db', {}).get('pods', {})
|
|
422
|
+
for uid, pod in pods.items():
|
|
423
|
+
port = pod['API_PORT_REST']
|
|
424
|
+
name = pod['NAME']
|
|
425
|
+
try:
|
|
426
|
+
requests.get(f"http://127.0.0.1:{port}/shutdown", timeout=3)
|
|
427
|
+
print(f" Shutdown: {name} ({pod['ROLE']}) on port {port}")
|
|
428
|
+
except requests.exceptions.ConnectionError:
|
|
429
|
+
print(f" Warning: Could not reach {name} on port {port}")
|
|
430
|
+
|
|
431
|
+
# Shutdown mgmt last
|
|
432
|
+
try:
|
|
433
|
+
requests.get(f"http://127.0.0.1:{mgmt_port}/shutdown", timeout=3)
|
|
434
|
+
print(f" Shutdown: mgmt on port {mgmt_port}")
|
|
435
|
+
except requests.exceptions.ConnectionError:
|
|
436
|
+
print(f" Warning: Could not reach mgmt on port {mgmt_port}")
|
|
437
|
+
|
|
438
|
+
print("Cluster shutdown complete.")
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def print_status_table(data, mgmt_port=42080):
|
|
442
|
+
"""Print a formatted status table from cluster info."""
|
|
443
|
+
pods = data.get('db', {}).get('pods', {})
|
|
444
|
+
|
|
445
|
+
print(f"{'ROLE':<16} {'NAME':<20} {'PORT':<8} {'STATUS':<10} {'VERSION'}")
|
|
446
|
+
print("-" * 70)
|
|
447
|
+
|
|
448
|
+
# Get mgmt info via /test endpoint
|
|
449
|
+
try:
|
|
450
|
+
mgmt_response = requests.get(f"http://127.0.0.1:{mgmt_port}/test", timeout=3)
|
|
451
|
+
mgmt_data = mgmt_response.json()
|
|
452
|
+
mgmt_name = mgmt_data.get('app', {}).get('name', '?')
|
|
453
|
+
mgmt_version = mgmt_data.get('app', {}).get('version', '?')
|
|
454
|
+
print(f"{'ubdcc-mgmt':<16} {mgmt_name:<20} {mgmt_port:<8} {'running':<10} {mgmt_version}")
|
|
455
|
+
except requests.exceptions.ConnectionError:
|
|
456
|
+
print(f"{'ubdcc-mgmt':<16} {'?':<20} {mgmt_port:<8} {'down':<10} {'?'}")
|
|
457
|
+
|
|
458
|
+
# Sort: restapi first, then dcn
|
|
459
|
+
role_order = {'ubdcc-restapi': 0, 'ubdcc-dcn': 1}
|
|
460
|
+
sorted_pods = sorted(pods.values(), key=lambda p: (role_order.get(p.get('ROLE', ''), 9), p.get('NAME', '')))
|
|
461
|
+
|
|
462
|
+
restapi_port = None
|
|
463
|
+
for pod in sorted_pods:
|
|
464
|
+
role = pod.get('ROLE', '?')
|
|
465
|
+
name = pod.get('NAME', '?')
|
|
466
|
+
port = pod.get('API_PORT_REST', '?')
|
|
467
|
+
status = pod.get('STATUS', '?')
|
|
468
|
+
version = pod.get('VERSION', '?')
|
|
469
|
+
print(f"{role:<16} {name:<20} {port:<8} {status:<10} {version}")
|
|
470
|
+
if role == 'ubdcc-restapi' and restapi_port is None:
|
|
471
|
+
restapi_port = port
|
|
472
|
+
|
|
473
|
+
depthcaches = data.get('db', {}).get('depthcaches', {})
|
|
474
|
+
dc_count = sum(len(markets) for markets in depthcaches.values())
|
|
475
|
+
print(f"\nDepthCaches: {dc_count}")
|
|
476
|
+
print(f"Version: {data.get('version', '?')}")
|
|
477
|
+
if restapi_port:
|
|
478
|
+
print(f"\nREST API: http://127.0.0.1:{restapi_port}/")
|
|
479
|
+
print(f"Cluster info: http://127.0.0.1:{restapi_port}/get_cluster_info")
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def main():
|
|
483
|
+
parser = argparse.ArgumentParser(
|
|
484
|
+
prog='ubdcc',
|
|
485
|
+
description='UNICORN Binance DepthCache Cluster — Cluster Manager\n'
|
|
486
|
+
'https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster',
|
|
487
|
+
epilog='Interactive shell commands (available inside "ubdcc start"):\n'
|
|
488
|
+
' add-dcn [count] Spawn new DCN process(es)\n'
|
|
489
|
+
' remove-dcn <count|name> Stop and remove DCN(s)\n'
|
|
490
|
+
' status Show cluster status\n'
|
|
491
|
+
' restart <name> Restart a specific pod\n'
|
|
492
|
+
' stop Shut down the cluster\n'
|
|
493
|
+
' help Show available shell commands',
|
|
494
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
495
|
+
)
|
|
496
|
+
parser.add_argument('-v', '--version', action='version', version=f'ubdcc {__version__}')
|
|
497
|
+
|
|
498
|
+
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
|
499
|
+
|
|
500
|
+
# start
|
|
501
|
+
start_parser = subparsers.add_parser('start', help='Start the cluster')
|
|
502
|
+
start_parser.add_argument('--dcn', type=int, default=1, help='Number of DCN processes (default: 1)')
|
|
503
|
+
start_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080 or next free)')
|
|
504
|
+
start_parser.add_argument('--logdir', type=str, default=None, help='Log directory (default: current directory)')
|
|
505
|
+
|
|
506
|
+
# status
|
|
507
|
+
status_parser = subparsers.add_parser('status', help='Show cluster status')
|
|
508
|
+
status_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080)')
|
|
509
|
+
|
|
510
|
+
# stop
|
|
511
|
+
stop_parser = subparsers.add_parser('stop', help='Stop the cluster')
|
|
512
|
+
stop_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080)')
|
|
513
|
+
|
|
514
|
+
# restart
|
|
515
|
+
restart_parser = subparsers.add_parser('restart', help='Restart a specific pod')
|
|
516
|
+
restart_parser.add_argument('name', help='Pod name or UID to restart')
|
|
517
|
+
restart_parser.add_argument('--port', type=int, default=None, help='Mgmt port (default: 42080)')
|
|
518
|
+
|
|
519
|
+
args = parser.parse_args()
|
|
520
|
+
|
|
521
|
+
if args.command == 'start':
|
|
522
|
+
cmd_start(args)
|
|
523
|
+
elif args.command == 'status':
|
|
524
|
+
cmd_status(args)
|
|
525
|
+
elif args.command == 'stop':
|
|
526
|
+
cmd_stop(args)
|
|
527
|
+
elif args.command == 'restart':
|
|
528
|
+
cmd_restart(args)
|
|
529
|
+
else:
|
|
530
|
+
parser.print_help()
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
if __name__ == "__main__":
|
|
534
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ubdcc
|
|
3
|
+
Version: 0.3.1
|
|
4
|
+
Summary: UNICORN Binance DepthCache Cluster — cluster manager and meta-package
|
|
5
|
+
Home-page: https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
|
|
6
|
+
Author: Oliver Zehentleitner
|
|
7
|
+
Author-email:
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Howto, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster#howto
|
|
10
|
+
Project-URL: Documentation, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster
|
|
11
|
+
Project-URL: Wiki, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/wiki
|
|
12
|
+
Project-URL: Author, https://www.linkedin.com/in/oliver-zehentleitner
|
|
13
|
+
Project-URL: Changes, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/packages/ubdcc/CHANGELOG.md
|
|
14
|
+
Project-URL: License, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/LICENSE
|
|
15
|
+
Project-URL: Issue Tracker, https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues
|
|
16
|
+
Project-URL: Telegram, https://t.me/unicorndevs
|
|
17
|
+
Keywords: binance,depth cache,cluster,order book
|
|
18
|
+
Classifier: Development Status :: 4 - Beta
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
26
|
+
Classifier: Intended Audience :: Developers
|
|
27
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
28
|
+
Classifier: Intended Audience :: Information Technology
|
|
29
|
+
Classifier: Intended Audience :: Science/Research
|
|
30
|
+
Classifier: Operating System :: OS Independent
|
|
31
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
32
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
33
|
+
Classifier: Framework :: AsyncIO
|
|
34
|
+
Requires-Python: >=3.9.0
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
License-File: LICENSE
|
|
37
|
+
Requires-Dist: ubdcc-mgmt
|
|
38
|
+
Requires-Dist: ubdcc-restapi
|
|
39
|
+
Requires-Dist: ubdcc-dcn
|
|
40
|
+
Dynamic: author
|
|
41
|
+
Dynamic: classifier
|
|
42
|
+
Dynamic: description
|
|
43
|
+
Dynamic: description-content-type
|
|
44
|
+
Dynamic: home-page
|
|
45
|
+
Dynamic: keywords
|
|
46
|
+
Dynamic: license
|
|
47
|
+
Dynamic: license-file
|
|
48
|
+
Dynamic: project-url
|
|
49
|
+
Dynamic: requires-dist
|
|
50
|
+
Dynamic: requires-python
|
|
51
|
+
Dynamic: summary
|
|
52
|
+
|
|
53
|
+
[](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/releases)
|
|
54
|
+
[](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/releases)
|
|
55
|
+
[](https://pypi.org/project/ubdcc/)
|
|
56
|
+
[](https://pepy.tech/project/ubdcc)
|
|
57
|
+
[](https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster/license.html)
|
|
58
|
+
[](https://www.python.org/downloads/)
|
|
59
|
+
[](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues)
|
|
60
|
+
[](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/actions/workflows/build_wheels_ubdcc.yml)
|
|
61
|
+
[](https://oliver-zehentleitner.github.io/unicorn-binance-depth-cache-cluster)
|
|
62
|
+
[](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/tree/master/packages/ubdcc)
|
|
63
|
+
[](https://t.me/unicorndevs)
|
|
64
|
+
|
|
65
|
+
# UNICORN Binance DepthCache Cluster
|
|
66
|
+
|
|
67
|
+
The `ubdcc` package is the all-in-one installer and cluster manager for the
|
|
68
|
+
[UNICORN Binance DepthCache Cluster](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster).
|
|
69
|
+
|
|
70
|
+
## What you get
|
|
71
|
+
|
|
72
|
+
- **All components installed**: `ubdcc-mgmt`, `ubdcc-restapi`, `ubdcc-dcn` and their dependencies
|
|
73
|
+
- **`ubdcc` command**: Cluster manager to start, stop and monitor your UBDCC instance (work in progress)
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install ubdcc
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This installs everything you need. Start the cluster:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ubdcc start --dcn 4
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This starts 1 mgmt + 1 restapi + 4 DCN processes and drops you into an interactive console where you can monitor
|
|
88
|
+
and manage the cluster (`status`, `stop`, `restart <name>`, `help`).
|
|
89
|
+
|
|
90
|
+
The REST API is available at `http://127.0.0.1:42081/`.
|
|
91
|
+
|
|
92
|
+
### Development (without pip install)
|
|
93
|
+
|
|
94
|
+
When working on the source code, run directly from the package directory:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
cd packages/ubdcc
|
|
98
|
+
python -m ubdcc start --dcn 4
|
|
99
|
+
python -m ubdcc status
|
|
100
|
+
python -m ubdcc stop
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For full documentation, architecture overview, REST API reference and Kubernetes setup, see the
|
|
104
|
+
[main project README](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster).
|
|
105
|
+
|
|
106
|
+
## How to report Bugs or suggest Improvements?
|
|
107
|
+
[List of planned features](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) - click  if you need one of them or suggest a new feature!
|
|
108
|
+
|
|
109
|
+
Before you report a bug, [try the latest release](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster#installation-and-upgrade). If the issue still exists, provide the error trace, OS
|
|
110
|
+
and Python version and explain how to reproduce the error. A demo script is appreciated.
|
|
111
|
+
|
|
112
|
+
If you don't find an issue related to your topic, please open a new [issue](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/issues)!
|
|
113
|
+
|
|
114
|
+
[Report a security bug!](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/security/policy)
|
|
115
|
+
|
|
116
|
+
## Contributing
|
|
117
|
+
[UNICORN Binance DepthCache Cluster](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster) is an open
|
|
118
|
+
source project which welcomes contributions which can be anything from simple documentation fixes and reporting dead links to new features. To
|
|
119
|
+
contribute follow
|
|
120
|
+
[this guide](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/blob/master/CONTRIBUTING.md).
|
|
121
|
+
|
|
122
|
+
### Contributors
|
|
123
|
+
[](https://github.com/oliver-zehentleitner/unicorn-binance-depth-cache-cluster/graphs/contributors)
|
|
124
|
+
|
|
125
|
+
We  open source!
|
|
126
|
+
|
|
127
|
+
## Disclaimer
|
|
128
|
+
This project is for informational purposes only. You should not construe this information or any other material as
|
|
129
|
+
legal, tax, investment, financial or other advice. Nothing contained herein constitutes a solicitation, recommendation,
|
|
130
|
+
endorsement or offer by us or any third party provider to buy or sell any securities or other financial instruments in
|
|
131
|
+
this or any other jurisdiction in which such solicitation or offer would be unlawful under the securities laws of such
|
|
132
|
+
jurisdiction.
|
|
133
|
+
|
|
134
|
+
### If you intend to use real money, use it at your own risk!
|
|
135
|
+
|
|
136
|
+
Under no circumstances will we be responsible or liable for any claims, damages, losses, expenses, costs or liabilities
|
|
137
|
+
of any kind, including but not limited to direct or indirect damages for loss of profits.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ubdcc/__init__.py,sha256=Jj7rFspXn8HVd2jNgKSHeNtIdjDBP1bf2CMIYak1OLM,704
|
|
2
|
+
ubdcc/__main__.py,sha256=0zN_oeN02xzMGvpVDK6l-9IDKKxcSknpKs8wM0ZVgnk,35
|
|
3
|
+
ubdcc/cli.py,sha256=pwWQIUEcJ0Y-XFIRcYVYzmbZr6wyHtQ0R2QTYxkj6jA,20124
|
|
4
|
+
ubdcc-0.3.1.dist-info/licenses/LICENSE,sha256=ucnbASA25rhSF9cHkE9Eb-q58uXbYLtdaZyyVkjSn9M,1123
|
|
5
|
+
ubdcc-0.3.1.dist-info/METADATA,sha256=Pr0pLk56bPh186F7s8voKjw8Usx9YDte0ZO68uu6ZjE,7868
|
|
6
|
+
ubdcc-0.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
7
|
+
ubdcc-0.3.1.dist-info/entry_points.txt,sha256=dtVonT-ML3jK0oxr2hD9hifJBzcaA2EGs5FsIc8f_Vs,41
|
|
8
|
+
ubdcc-0.3.1.dist-info/top_level.txt,sha256=xUIKyETAc7TVepxCLkHXfiD8-e65oNVs52VGSRPO_jU,6
|
|
9
|
+
ubdcc-0.3.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026, Oliver Zehentleitner (https://about.me/oliver-zehentleitner)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ubdcc
|