dsmq 1.3.0__py3-none-any.whl → 1.4.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.
- dsmq/.client.py.swp +0 -0
- dsmq/.server.py.swp +0 -0
- dsmq/client.py +34 -2
- dsmq/demo.py +8 -3
- dsmq/server.py +179 -119
- dsmq/tests/integration_test.py +44 -17
- dsmq/tests/performance_suite.py +11 -2
- {dsmq-1.3.0.dist-info → dsmq-1.4.0.dist-info}/METADATA +11 -1
- dsmq-1.4.0.dist-info/RECORD +15 -0
- dsmq/tests/.performance_suite.py.swp +0 -0
- dsmq-1.3.0.dist-info/RECORD +0 -15
- {dsmq-1.3.0.dist-info → dsmq-1.4.0.dist-info}/WHEEL +0 -0
- {dsmq-1.3.0.dist-info → dsmq-1.4.0.dist-info}/licenses/LICENSE +0 -0
dsmq/.client.py.swp
ADDED
Binary file
|
dsmq/.server.py.swp
CHANGED
Binary file
|
dsmq/client.py
CHANGED
@@ -18,6 +18,7 @@ def connect(host=_default_host, port=_default_port, verbose=False):
|
|
18
18
|
class DSMQClientSideConnection:
|
19
19
|
def __init__(self, host, port, verbose=False):
|
20
20
|
self.uri = f"ws://{host}:{port}"
|
21
|
+
self.port = port
|
21
22
|
self.verbose = verbose
|
22
23
|
if self.verbose:
|
23
24
|
print(f"Connecting to dsmq server at {self.uri}")
|
@@ -40,13 +41,41 @@ class DSMQClientSideConnection:
|
|
40
41
|
|
41
42
|
def get(self, topic):
|
42
43
|
msg = {"action": "get", "topic": topic}
|
43
|
-
|
44
|
+
try:
|
45
|
+
self.websocket.send(json.dumps(msg))
|
46
|
+
except ConnectionClosedError:
|
47
|
+
return ""
|
48
|
+
|
49
|
+
try:
|
50
|
+
msg_text = self.websocket.recv()
|
51
|
+
except ConnectionClosedError:
|
52
|
+
self.close()
|
53
|
+
return ""
|
54
|
+
|
55
|
+
msg = json.loads(msg_text)
|
56
|
+
return msg["message"]
|
57
|
+
|
58
|
+
def get_latest(self, topic):
|
59
|
+
"""
|
60
|
+
A variant of `get()` that grabs the latest available message
|
61
|
+
(if there is one) rather than grabbing the oldest unread message.
|
62
|
+
It will not go back to read older ones on subsequent calls;
|
63
|
+
it will leave them unread.
|
64
|
+
"""
|
65
|
+
msg = {"action": "get_latest", "topic": topic}
|
66
|
+
try:
|
67
|
+
self.websocket.send(json.dumps(msg))
|
68
|
+
except ConnectionClosedError:
|
69
|
+
return ""
|
70
|
+
|
44
71
|
try:
|
45
72
|
msg_text = self.websocket.recv()
|
46
73
|
except ConnectionClosedError:
|
47
74
|
self.close()
|
75
|
+
return ""
|
48
76
|
|
49
77
|
msg = json.loads(msg_text)
|
78
|
+
|
50
79
|
return msg["message"]
|
51
80
|
|
52
81
|
def get_wait(self, topic):
|
@@ -64,7 +93,10 @@ class DSMQClientSideConnection:
|
|
64
93
|
|
65
94
|
def put(self, topic, msg_body):
|
66
95
|
msg_dict = {"action": "put", "topic": topic, "message": msg_body}
|
67
|
-
|
96
|
+
try:
|
97
|
+
self.websocket.send(json.dumps(msg_dict))
|
98
|
+
except ConnectionClosedError:
|
99
|
+
return
|
68
100
|
|
69
101
|
def shutdown_server(self):
|
70
102
|
msg_dict = {"action": "shutdown", "topic": ""}
|
dsmq/demo.py
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
import multiprocessing as mp
|
2
|
-
|
3
|
-
|
4
|
-
|
2
|
+
|
3
|
+
# spawn is the default method on macOS,
|
4
|
+
# starting in Python 3.14 it will be the default in Linux too.
|
5
|
+
mp.set_start_method("spawn")
|
6
|
+
|
7
|
+
from dsmq.server import serve # noqa: E402
|
8
|
+
import dsmq.example_get_client # noqa: E402
|
9
|
+
import dsmq.example_put_client # noqa: E402
|
5
10
|
|
6
11
|
HOST = "127.0.0.1"
|
7
12
|
PORT = 25252
|
dsmq/server.py
CHANGED
@@ -9,48 +9,47 @@ from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
|
|
9
9
|
|
10
10
|
_default_host = "127.0.0.1"
|
11
11
|
_default_port = 30008
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# _db_name = ":memory:"
|
17
|
-
_db_name = "file::memory:?cache=shared"
|
18
|
-
# May occasionally create files with this name.
|
19
|
-
# https://sqlite.org/inmemorydb.html
|
20
|
-
# "...parts of a temporary database might be flushed to disk if the
|
21
|
-
# database becomes large or if SQLite comes under memory pressure."
|
12
|
+
_max_queue_length = 10
|
13
|
+
_shutdown_pause = 1.0 # seconds
|
14
|
+
_time_between_cleanup = 3.0 # seconds
|
15
|
+
_time_to_keep = 0.3 # seconds
|
22
16
|
|
23
17
|
# Make this global so it's easy to share
|
24
18
|
dsmq_server = None
|
25
19
|
|
26
20
|
|
27
|
-
def serve(
|
21
|
+
def serve(
|
22
|
+
host=_default_host,
|
23
|
+
port=_default_port,
|
24
|
+
name="mqdb",
|
25
|
+
verbose=False,
|
26
|
+
):
|
28
27
|
"""
|
29
28
|
For best results, start this running in its own process and walk away.
|
30
29
|
"""
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# rapid-fire one-item reads and writes.
|
40
|
-
filenames = os.listdir()
|
41
|
-
for filename in filenames:
|
42
|
-
if filename[: len(_db_name)] == _db_name:
|
43
|
-
os.remove(filename)
|
44
|
-
|
30
|
+
# May occasionally create files with this name.
|
31
|
+
# https://sqlite.org/inmemorydb.html
|
32
|
+
# "...parts of a temporary database might be flushed to disk if the
|
33
|
+
# database becomes large or if SQLite comes under memory pressure."
|
34
|
+
global _db_name
|
35
|
+
_db_name = f"file:{name}?mode=memory&cache=shared"
|
36
|
+
|
37
|
+
cleanup_temp_files()
|
45
38
|
sqlite_conn = sqlite3.connect(_db_name)
|
46
39
|
cursor = sqlite_conn.cursor()
|
47
40
|
|
48
41
|
# Tweak the connection to make it faster
|
49
42
|
# and keep long-term latency more predictable.
|
50
|
-
#
|
51
|
-
|
52
|
-
#
|
53
|
-
#
|
43
|
+
# These also make it more susceptible to corruption during shutdown,
|
44
|
+
# but since dsmq is meant to be ephemeral, that's not a concern.
|
45
|
+
# See https://www.sqlite.org/pragma.html
|
46
|
+
#
|
47
|
+
cursor.execute("PRAGMA journal_mode = OFF")
|
48
|
+
# cursor.execute("PRAGMA journal_mode = MEMORY")
|
49
|
+
# cursor.execute("PRAGMA synchronous = NORMAL")
|
50
|
+
cursor.execute("PRAGMA synchronous = OFF")
|
51
|
+
cursor.execute("PRAGMA secure_delete = OFF")
|
52
|
+
cursor.execute("PRAGMA temp_store = MEMORY")
|
54
53
|
|
55
54
|
cursor.execute("""
|
56
55
|
CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
|
@@ -58,44 +57,60 @@ CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
|
|
58
57
|
|
59
58
|
# Making this global in scope is a way to make it available
|
60
59
|
# to the shutdown operation. It's an awkward construction,
|
61
|
-
# and a method of last resort.
|
62
|
-
# figure out something more elegant, please submit a PR!
|
63
|
-
# or send it to me at brohrer@gmail.com,
|
60
|
+
# and a method of last resort.
|
64
61
|
global dsmq_server
|
65
62
|
|
66
|
-
|
67
|
-
|
68
|
-
|
63
|
+
try:
|
64
|
+
with ws_serve(request_handler, host, port) as dsmq_server:
|
65
|
+
dsmq_server.serve_forever()
|
66
|
+
|
67
|
+
except OSError:
|
68
|
+
# Catch the case where the address is already in use
|
69
|
+
if verbose:
|
70
|
+
print()
|
71
|
+
print(f"Found a dsmq server already running on {host} on port {port}.")
|
72
|
+
print(" Closing it down.")
|
73
|
+
|
74
|
+
def shutdown_gracefully(server_to_shutdown):
|
75
|
+
server_to_shutdown.shutdown()
|
76
|
+
|
77
|
+
Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
|
78
|
+
time.sleep(_shutdown_pause)
|
79
|
+
|
69
80
|
with ws_serve(request_handler, host, port) as dsmq_server:
|
70
81
|
dsmq_server.serve_forever()
|
71
82
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
break
|
78
|
-
|
79
|
-
except OSError:
|
80
|
-
# Catch the case where the address is already in use
|
81
|
-
if verbose:
|
82
|
-
print()
|
83
|
-
if i_retry < _n_retries - 1:
|
84
|
-
print(f"Couldn't start dsmq server on {host} on port {port}.")
|
85
|
-
print(f" Trying again ({i_retry}) ...")
|
86
|
-
else:
|
87
|
-
print()
|
88
|
-
print(f"Failed to start dsmq server on {host} on port {port}.")
|
89
|
-
print()
|
90
|
-
raise
|
91
|
-
|
92
|
-
wait_time = _first_retry * 2**i_retry
|
93
|
-
time.sleep(wait_time)
|
83
|
+
if verbose:
|
84
|
+
print()
|
85
|
+
print(f"Server started at {host} on port {port}.")
|
86
|
+
print("Waiting for clients...")
|
94
87
|
|
95
88
|
sqlite_conn.close()
|
89
|
+
time.sleep(_shutdown_pause)
|
90
|
+
cleanup_temp_files()
|
91
|
+
|
92
|
+
|
93
|
+
def cleanup_temp_files():
|
94
|
+
# Under some condition
|
95
|
+
# (which I haven't yet been able to pin down)
|
96
|
+
# a file is generated with the db name.
|
97
|
+
# If it is not removed, it gets
|
98
|
+
# treated as a SQLite db on disk,
|
99
|
+
# which dramatically slows it down,
|
100
|
+
# especially the way it's used here for
|
101
|
+
# rapid-fire one-item reads and writes.
|
102
|
+
global _db_name
|
103
|
+
filenames = os.listdir()
|
104
|
+
for filename in filenames:
|
105
|
+
if filename[: len(_db_name)] == _db_name:
|
106
|
+
try:
|
107
|
+
os.remove(filename)
|
108
|
+
except FileNotFoundError:
|
109
|
+
pass
|
96
110
|
|
97
111
|
|
98
112
|
def request_handler(websocket):
|
113
|
+
global _db_name
|
99
114
|
sqlite_conn = sqlite3.connect(_db_name)
|
100
115
|
cursor = sqlite_conn.cursor()
|
101
116
|
|
@@ -107,30 +122,25 @@ def request_handler(websocket):
|
|
107
122
|
for msg_text in websocket:
|
108
123
|
msg = json.loads(msg_text)
|
109
124
|
topic = msg["topic"]
|
125
|
+
action = msg["action"]
|
110
126
|
timestamp = time.time()
|
111
127
|
|
112
|
-
if
|
128
|
+
if action == "put":
|
113
129
|
msg["timestamp"] = timestamp
|
114
130
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
wait_time = _first_retry * 2**i_retry
|
129
|
-
time.sleep(wait_time)
|
130
|
-
continue
|
131
|
-
break
|
132
|
-
|
133
|
-
elif msg["action"] == "get":
|
131
|
+
try:
|
132
|
+
cursor.execute(
|
133
|
+
"""
|
134
|
+
INSERT INTO messages (timestamp, topic, message)
|
135
|
+
VALUES (:timestamp, :topic, :message)
|
136
|
+
""",
|
137
|
+
(msg),
|
138
|
+
)
|
139
|
+
sqlite_conn.commit()
|
140
|
+
except sqlite3.OperationalError:
|
141
|
+
pass
|
142
|
+
|
143
|
+
elif action == "get":
|
134
144
|
try:
|
135
145
|
last_read_time = last_read_times[topic]
|
136
146
|
except KeyError:
|
@@ -138,31 +148,21 @@ def request_handler(websocket):
|
|
138
148
|
last_read_time = last_read_times[topic]
|
139
149
|
msg["last_read_time"] = last_read_time
|
140
150
|
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
WHERE topic = :topic
|
157
|
-
AND timestamp = a.min_time
|
158
|
-
""",
|
159
|
-
msg,
|
160
|
-
)
|
161
|
-
except sqlite3.OperationalError:
|
162
|
-
wait_time = _first_retry * 2**i_retry
|
163
|
-
time.sleep(wait_time)
|
164
|
-
continue
|
165
|
-
break
|
151
|
+
try:
|
152
|
+
cursor.execute(
|
153
|
+
"""
|
154
|
+
SELECT message,
|
155
|
+
timestamp
|
156
|
+
FROM messages
|
157
|
+
WHERE topic = :topic
|
158
|
+
AND timestamp > :last_read_time
|
159
|
+
ORDER BY timestamp ASC
|
160
|
+
LIMIT 1
|
161
|
+
""",
|
162
|
+
msg,
|
163
|
+
)
|
164
|
+
except sqlite3.OperationalError:
|
165
|
+
pass
|
166
166
|
|
167
167
|
try:
|
168
168
|
result = cursor.fetchall()[0]
|
@@ -174,45 +174,105 @@ def request_handler(websocket):
|
|
174
174
|
message = ""
|
175
175
|
|
176
176
|
websocket.send(json.dumps({"message": message}))
|
177
|
-
|
177
|
+
|
178
|
+
elif action == "get_latest":
|
179
|
+
try:
|
180
|
+
last_read_time = last_read_times[topic]
|
181
|
+
except KeyError:
|
182
|
+
last_read_times[topic] = client_creation_time
|
183
|
+
last_read_time = last_read_times[topic]
|
184
|
+
msg["last_read_time"] = last_read_time
|
185
|
+
|
186
|
+
try:
|
187
|
+
cursor.execute(
|
188
|
+
"""
|
189
|
+
SELECT message,
|
190
|
+
timestamp
|
191
|
+
FROM messages
|
192
|
+
WHERE topic = :topic
|
193
|
+
AND timestamp > :last_read_time
|
194
|
+
ORDER BY timestamp DESC
|
195
|
+
LIMIT 1;
|
196
|
+
""",
|
197
|
+
msg,
|
198
|
+
)
|
199
|
+
except sqlite3.OperationalError:
|
200
|
+
pass
|
201
|
+
|
202
|
+
try:
|
203
|
+
result = cursor.fetchall()[0]
|
204
|
+
message = result[0]
|
205
|
+
timestamp = result[1]
|
206
|
+
last_read_times[topic] = timestamp
|
207
|
+
except IndexError:
|
208
|
+
# Handle the case where no results are returned
|
209
|
+
message = ""
|
210
|
+
|
211
|
+
websocket.send(json.dumps({"message": message}))
|
212
|
+
|
213
|
+
elif action == "shutdown":
|
178
214
|
# Run this from a separate thread to prevent deadlock
|
179
215
|
global dsmq_server
|
180
216
|
|
181
217
|
def shutdown_gracefully(server_to_shutdown):
|
182
218
|
server_to_shutdown.shutdown()
|
183
219
|
|
184
|
-
filenames = os.listdir()
|
185
|
-
for filename in filenames:
|
186
|
-
if filename[: len(_db_name)] == _db_name:
|
187
|
-
try:
|
188
|
-
os.remove(filename)
|
189
|
-
except FileNotFoundError:
|
190
|
-
pass
|
191
|
-
|
192
220
|
Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
|
193
221
|
break
|
194
222
|
else:
|
195
223
|
raise RuntimeWarning(
|
196
|
-
"dsmq client action must either be
|
224
|
+
"dsmq client action must either be\n"
|
225
|
+
+ "'put', 'get', 'get_wait', 'get_latest', or 'shutdown'"
|
197
226
|
)
|
198
227
|
|
199
|
-
# Periodically clean out messages
|
200
|
-
#
|
201
|
-
|
202
|
-
|
228
|
+
# Periodically clean out messages to keep individual queues at
|
229
|
+
# a manageable length and the overall mq small.
|
230
|
+
if time.time() - time_of_last_purge > _time_between_cleanup:
|
231
|
+
cutoff_time = time.time() - _time_to_keep
|
203
232
|
try:
|
204
233
|
cursor.execute(
|
205
234
|
"""
|
206
|
-
DELETE
|
207
|
-
|
235
|
+
DELETE
|
236
|
+
FROM messages
|
237
|
+
WHERE topic = :topic
|
238
|
+
AND timestamp < :cutoff_time
|
208
239
|
""",
|
209
|
-
{
|
240
|
+
{
|
241
|
+
"cutoff_time": cutoff_time,
|
242
|
+
"topic": topic,
|
243
|
+
},
|
210
244
|
)
|
211
245
|
sqlite_conn.commit()
|
212
246
|
time_of_last_purge = time.time()
|
247
|
+
|
248
|
+
cursor.execute(
|
249
|
+
"""
|
250
|
+
DELETE
|
251
|
+
FROM messages
|
252
|
+
WHERE topic = :topic
|
253
|
+
AND timestamp IN (
|
254
|
+
SELECT timestamp
|
255
|
+
FROM (
|
256
|
+
SELECT timestamp,
|
257
|
+
RANK() OVER (ORDER BY timestamp DESC) recency_rank
|
258
|
+
FROM messages
|
259
|
+
WHERE topic = :topic
|
260
|
+
)
|
261
|
+
WHERE recency_rank >= :max_queue_length
|
262
|
+
)
|
263
|
+
""",
|
264
|
+
{
|
265
|
+
"max_queue_length": _max_queue_length,
|
266
|
+
"topic": topic,
|
267
|
+
},
|
268
|
+
)
|
269
|
+
sqlite_conn.commit()
|
270
|
+
time_of_last_purge = time.time()
|
271
|
+
|
213
272
|
except sqlite3.OperationalError:
|
214
273
|
# Database may be locked. Try again next time.
|
215
274
|
pass
|
275
|
+
|
216
276
|
except (ConnectionClosedError, ConnectionClosedOK):
|
217
277
|
# Something happened on the other end and this handler
|
218
278
|
# is no longer needed.
|
dsmq/tests/integration_test.py
CHANGED
@@ -1,10 +1,17 @@
|
|
1
1
|
import multiprocessing as mp
|
2
|
+
|
3
|
+
try:
|
4
|
+
# spawn is the default method on macOS,
|
5
|
+
# starting in Python 3.14 it will be the default in Linux too.
|
6
|
+
mp.set_start_method("spawn")
|
7
|
+
except RuntimeError:
|
8
|
+
# Will throw an error if the start method has alraedy been set.
|
9
|
+
pass
|
10
|
+
|
2
11
|
import time
|
3
12
|
from dsmq.server import serve
|
4
13
|
from dsmq.client import connect
|
5
14
|
|
6
|
-
# spawn is the default method on macOS
|
7
|
-
# mp.set_start_method('spawn')
|
8
15
|
|
9
16
|
host = "127.0.0.1"
|
10
17
|
port = 30303
|
@@ -53,6 +60,10 @@ def test_write_one_read_one():
|
|
53
60
|
write_client = connect(host, port)
|
54
61
|
read_client = connect(host, port)
|
55
62
|
|
63
|
+
msg = read_client.get("test")
|
64
|
+
|
65
|
+
assert msg == ""
|
66
|
+
|
56
67
|
write_client.put("test", "test_msg")
|
57
68
|
|
58
69
|
# It takes a moment for the write to complete
|
@@ -61,6 +72,10 @@ def test_write_one_read_one():
|
|
61
72
|
|
62
73
|
assert msg == "test_msg"
|
63
74
|
|
75
|
+
msg = read_client.get("test")
|
76
|
+
|
77
|
+
assert msg == ""
|
78
|
+
|
64
79
|
write_client.shutdown_server()
|
65
80
|
write_client.close()
|
66
81
|
read_client.close()
|
@@ -83,6 +98,29 @@ def test_get_wait():
|
|
83
98
|
read_client.close()
|
84
99
|
|
85
100
|
|
101
|
+
def test_get_latest():
|
102
|
+
p_server = mp.Process(target=serve, args=(host, port))
|
103
|
+
p_server.start()
|
104
|
+
write_client = connect(host, port)
|
105
|
+
read_client = connect(host, port)
|
106
|
+
|
107
|
+
for i in range(5):
|
108
|
+
write_client.put("test", f"test_msg {i}")
|
109
|
+
|
110
|
+
time.sleep(_pause)
|
111
|
+
msg = read_client.get_latest("test")
|
112
|
+
|
113
|
+
assert msg == "test_msg 4"
|
114
|
+
|
115
|
+
msg = read_client.get_latest("test")
|
116
|
+
|
117
|
+
assert msg == ""
|
118
|
+
|
119
|
+
write_client.shutdown_server()
|
120
|
+
write_client.close()
|
121
|
+
read_client.close()
|
122
|
+
|
123
|
+
|
86
124
|
def test_multitopics():
|
87
125
|
p_server = mp.Process(target=serve, args=(host, port))
|
88
126
|
p_server.start()
|
@@ -183,15 +221,8 @@ def test_speed_writing():
|
|
183
221
|
p_speed_write.start()
|
184
222
|
time.sleep(_pause)
|
185
223
|
|
186
|
-
# time_a = time.time()
|
187
224
|
write_client.put("test", "test_msg")
|
188
|
-
# time_b = time.time()
|
189
225
|
msg = read_client.get_wait("test")
|
190
|
-
# time_c = time.time()
|
191
|
-
|
192
|
-
# write_time = int((time_b - time_a) * 1e6)
|
193
|
-
# read_time = int((time_c - time_b) * 1e6)
|
194
|
-
# print(f"write time: {write_time} us, read time: {read_time} us")
|
195
226
|
|
196
227
|
assert msg == "test_msg"
|
197
228
|
|
@@ -221,15 +252,8 @@ def test_speed_reading():
|
|
221
252
|
p_speed_read = mp.Process(target=speed_read, args=(stop_flag,))
|
222
253
|
p_speed_read.start()
|
223
254
|
|
224
|
-
# time_a = time.time()
|
225
255
|
write_client.put("test", "test_msg")
|
226
|
-
# time_b = time.time()
|
227
256
|
msg = read_client.get_wait("test")
|
228
|
-
# time_c = time.time()
|
229
|
-
|
230
|
-
# write_time = int((time_b - time_a) * 1e6)
|
231
|
-
# read_time = int((time_c - time_b) * 1e6)
|
232
|
-
# print(f"write time: {write_time} us, read time: {read_time} us")
|
233
257
|
|
234
258
|
assert msg == "test_msg"
|
235
259
|
|
@@ -238,5 +262,8 @@ def test_speed_reading():
|
|
238
262
|
p_speed_read.kill()
|
239
263
|
read_client.close()
|
240
264
|
write_client.shutdown_server()
|
241
|
-
# time.sleep(_pause)
|
242
265
|
write_client.close()
|
266
|
+
|
267
|
+
|
268
|
+
if __name__ == "__main__":
|
269
|
+
test_get_latest()
|
dsmq/tests/performance_suite.py
CHANGED
@@ -1,4 +1,13 @@
|
|
1
1
|
import multiprocessing as mp
|
2
|
+
|
3
|
+
# spawn is the default method on macOS,
|
4
|
+
# starting in Python 3.14 it will be the default in Linux too.
|
5
|
+
try:
|
6
|
+
mp.set_start_method("spawn")
|
7
|
+
except RuntimeError as e:
|
8
|
+
print(e)
|
9
|
+
print(f"Using multiprocessing start method: {mp.get_start_method()}")
|
10
|
+
|
2
11
|
import time
|
3
12
|
|
4
13
|
from dsmq.server import serve
|
@@ -11,8 +20,8 @@ verbose = False
|
|
11
20
|
_pause = 0.01
|
12
21
|
_very_long_pause = 1.0
|
13
22
|
|
14
|
-
_n_iter = int(
|
15
|
-
_n_long_char = int(
|
23
|
+
_n_iter = int(1e4)
|
24
|
+
_n_long_char = int(1e5)
|
16
25
|
|
17
26
|
_short_msg = "q"
|
18
27
|
_long_msg = str(["q"] * _n_long_char)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: dsmq
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.4.0
|
4
4
|
Summary: A dead simple message queue
|
5
5
|
License-File: LICENSE
|
6
6
|
Requires-Python: >=3.10
|
@@ -152,6 +152,16 @@ connected to the server.
|
|
152
152
|
in the topic, or the topic doesn't yet exist,
|
153
153
|
returns `""`.
|
154
154
|
|
155
|
+
### `get_latest(topic)`
|
156
|
+
|
157
|
+
Get the *most recent* eligible message from the queue named `topic`.
|
158
|
+
All the messages older than that in the queue become ineligible and never
|
159
|
+
get seen by the client.
|
160
|
+
- `topic` (str)
|
161
|
+
- returns str, the content of the message. If there was no eligble message
|
162
|
+
in the topic, or the topic doesn't yet exist,
|
163
|
+
returns `""`.
|
164
|
+
|
155
165
|
### `get_wait(topic)`
|
156
166
|
|
157
167
|
A variant of `get()` that retries a few times until it gets
|
@@ -0,0 +1,15 @@
|
|
1
|
+
dsmq/.client.py.swp,sha256=bm5ybp3ESthgsOMpHRygMeQWbSlK4xw8toIKiT5A0pM,12288
|
2
|
+
dsmq/.server.py.swp,sha256=R61qwjXxZUZAbNpEXSW_jvxkXvy4qqt7gA1FEzwE-jk,20480
|
3
|
+
dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
|
4
|
+
dsmq/client.py,sha256=a4_6jcF7RAC58And0OlR3vL42FgZ24q2BECS-VF4Ri4,3438
|
5
|
+
dsmq/demo.py,sha256=x2ueymZnSQNtQ-HjzRiSCa0rg4NOm0YUIaj5VPXfnD8,718
|
6
|
+
dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
|
7
|
+
dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
|
8
|
+
dsmq/server.py,sha256=yANPboqqoAw7GnSyHs_SeYlZjKkQPDMIhRo7tGDw6zo,10041
|
9
|
+
dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
+
dsmq/tests/integration_test.py,sha256=zF6v_3UHt5KqveX22gEpCSLcoeOyEWLSy_gMLoytg2U,6303
|
11
|
+
dsmq/tests/performance_suite.py,sha256=LgNpqD0zumEOgrNDLkeirBmQAALhlxEH7bXe4z-EuIY,5474
|
12
|
+
dsmq-1.4.0.dist-info/METADATA,sha256=RTvtHRtUg4HSOvyMRTu7sFy3uu3Ddd7iOXD1IWK91QQ,5203
|
13
|
+
dsmq-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
14
|
+
dsmq-1.4.0.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
|
15
|
+
dsmq-1.4.0.dist-info/RECORD,,
|
Binary file
|
dsmq-1.3.0.dist-info/RECORD
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
dsmq/.server.py.swp,sha256=RLjzmw9FyOQY73HurM9PEjp4meAgVEZIN8M92OqgBzw,28672
|
2
|
-
dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
|
3
|
-
dsmq/client.py,sha256=_JJlDFTw1-VsRNsm-grYvexrWkkh50n62Q6-aGWum5Q,2547
|
4
|
-
dsmq/demo.py,sha256=K53cC5kN7K4kNJlPq7c5OTIMHRCKTo9hYX2aIos57rU,542
|
5
|
-
dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
|
6
|
-
dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
|
7
|
-
dsmq/server.py,sha256=Ej6iQ1aw4LVHyHvdeZxxwSZS3VBIQP0qsFstuo0hwnY,8487
|
8
|
-
dsmq/tests/.performance_suite.py.swp,sha256=D3B86JpgBIYDE0at6nG2Uw9WWASFxp12mB9zlNXYPbA,24576
|
9
|
-
dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
10
|
-
dsmq/tests/integration_test.py,sha256=dLsQGCmpXv4zRb93TriccH7TbUyD9MHcLckAQqfDOK4,5980
|
11
|
-
dsmq/tests/performance_suite.py,sha256=E59zB2ZvM8V5f8RxaB7p-Kehqyhrgsl0sXuy7g74BaI,5218
|
12
|
-
dsmq-1.3.0.dist-info/METADATA,sha256=bZRJ0Oz5Vnavl4AU9bO2f7TuktSpUOJQ9hyx7Yusne4,4859
|
13
|
-
dsmq-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
14
|
-
dsmq-1.3.0.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
|
15
|
-
dsmq-1.3.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|