dsmq 1.3.1__py3-none-any.whl → 1.4.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.
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,7 +41,11 @@ class DSMQClientSideConnection:
40
41
 
41
42
  def get(self, topic):
42
43
  msg = {"action": "get", "topic": topic}
43
- self.websocket.send(json.dumps(msg))
44
+ try:
45
+ self.websocket.send(json.dumps(msg))
46
+ except ConnectionClosedError:
47
+ return ""
48
+
44
49
  try:
45
50
  msg_text = self.websocket.recv()
46
51
  except ConnectionClosedError:
@@ -50,6 +55,29 @@ class DSMQClientSideConnection:
50
55
  msg = json.loads(msg_text)
51
56
  return msg["message"]
52
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
+
71
+ try:
72
+ msg_text = self.websocket.recv()
73
+ except ConnectionClosedError:
74
+ self.close()
75
+ return ""
76
+
77
+ msg = json.loads(msg_text)
78
+
79
+ return msg["message"]
80
+
53
81
  def get_wait(self, topic):
54
82
  """
55
83
  A variant of `get()` that retries a few times until it gets
@@ -65,7 +93,10 @@ class DSMQClientSideConnection:
65
93
 
66
94
  def put(self, topic, msg_body):
67
95
  msg_dict = {"action": "put", "topic": topic, "message": msg_body}
68
- self.websocket.send(json.dumps(msg_dict))
96
+ try:
97
+ self.websocket.send(json.dumps(msg_dict))
98
+ except ConnectionClosedError:
99
+ return
69
100
 
70
101
  def shutdown_server(self):
71
102
  msg_dict = {"action": "shutdown", "topic": ""}
dsmq/demo.py CHANGED
@@ -1,7 +1,16 @@
1
1
  import multiprocessing as mp
2
- from dsmq.server import serve
3
- import dsmq.example_get_client
4
- import dsmq.example_put_client
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:
8
+ # Will throw an error if the start method has alraedy been set.
9
+ pass
10
+
11
+ from dsmq.server import serve # noqa: E402
12
+ import dsmq.example_get_client # noqa: E402
13
+ import dsmq.example_put_client # noqa: E402
5
14
 
6
15
  HOST = "127.0.0.1"
7
16
  PORT = 25252
dsmq/server.py CHANGED
@@ -9,10 +9,12 @@ from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
9
9
 
10
10
  _default_host = "127.0.0.1"
11
11
  _default_port = 30008
12
- _n_retries = 20
13
- _first_retry = 0.005 # seconds
14
- _time_between_cleanup = 0.05 # seconds
15
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
16
+ # _time_between_cleanup = 60.0 # seconds
17
+ # _time_to_keep = 10.0 # seconds
16
18
 
17
19
  # Make this global so it's easy to share
18
20
  dsmq_server = None
@@ -42,10 +44,15 @@ def serve(
42
44
  # and keep long-term latency more predictable.
43
45
  # These also make it more susceptible to corruption during shutdown,
44
46
  # but since dsmq is meant to be ephemeral, that's not a concern.
45
- cursor.execute("PRAGMA journal_mode = OFF")
46
- cursor.execute("PRAGMA synchronous = OFF")
47
- cursor.execute("PRAGMA secure_delete = OFF")
48
- cursor.execute("PRAGMA temp_store = MEMORY")
47
+ # See https://www.sqlite.org/pragma.html
48
+ # After playing around with them, I'm not sure these have any noticeable effect.
49
+ #
50
+ # cursor.execute("PRAGMA journal_mode = OFF")
51
+ # cursor.execute("PRAGMA journal_mode = MEMORY")
52
+ # cursor.execute("PRAGMA synchronous = NORMAL")
53
+ # cursor.execute("PRAGMA synchronous = OFF")
54
+ # cursor.execute("PRAGMA secure_delete = OFF")
55
+ # cursor.execute("PRAGMA temp_store = MEMORY")
49
56
 
50
57
  cursor.execute("""
51
58
  CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
@@ -56,36 +63,33 @@ CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
56
63
  # and a method of last resort.
57
64
  global dsmq_server
58
65
 
59
- for i_retry in range(_n_retries):
60
- try:
66
+ try:
67
+ with ws_serve(request_handler, host, port) as dsmq_server:
68
+ dsmq_server.serve_forever()
69
+
70
+ except OSError:
71
+ # Catch the case where the address is already in use
72
+ if verbose:
73
+ print()
74
+ print(f"Found a dsmq server already running on {host} on port {port}.")
75
+ print(" Closing it down.")
76
+
77
+ def shutdown_gracefully(server_to_shutdown):
78
+ server_to_shutdown.shutdown()
79
+
80
+ Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
81
+ time.sleep(_shutdown_pause)
82
+
61
83
  with ws_serve(request_handler, host, port) as dsmq_server:
62
84
  dsmq_server.serve_forever()
63
85
 
64
- if verbose:
65
- print()
66
- print(f"Server started at {host} on port {port}.")
67
- print("Waiting for clients...")
68
-
69
- break
70
-
71
- except OSError:
72
- # Catch the case where the address is already in use
73
- if verbose:
74
- print()
75
- if i_retry < _n_retries - 1:
76
- print(f"Couldn't start dsmq server on {host} on port {port}.")
77
- print(f" Trying again ({i_retry}) ...")
78
- else:
79
- print()
80
- print(f"Failed to start dsmq server on {host} on port {port}.")
81
- print()
82
- raise
83
-
84
- wait_time = _first_retry * 2**i_retry
85
- time.sleep(wait_time)
86
+ if verbose:
87
+ print()
88
+ print(f"Server started at {host} on port {port}.")
89
+ print("Waiting for clients...")
86
90
 
87
91
  sqlite_conn.close()
88
- time.sleep(_time_between_cleanup)
92
+ time.sleep(_shutdown_pause)
89
93
  cleanup_temp_files()
90
94
 
91
95
 
@@ -121,9 +125,10 @@ def request_handler(websocket):
121
125
  for msg_text in websocket:
122
126
  msg = json.loads(msg_text)
123
127
  topic = msg["topic"]
128
+ action = msg["action"]
124
129
  timestamp = time.time()
125
130
 
126
- if msg["action"] == "put":
131
+ if action == "put":
127
132
  msg["timestamp"] = timestamp
128
133
 
129
134
  try:
@@ -138,7 +143,7 @@ def request_handler(websocket):
138
143
  except sqlite3.OperationalError:
139
144
  pass
140
145
 
141
- elif msg["action"] == "get":
146
+ elif action == "get":
142
147
  try:
143
148
  last_read_time = last_read_times[topic]
144
149
  except KeyError:
@@ -151,15 +156,46 @@ def request_handler(websocket):
151
156
  """
152
157
  SELECT message,
153
158
  timestamp
154
- FROM messages,
155
- (
156
- SELECT MIN(timestamp) AS min_time
157
159
  FROM messages
158
160
  WHERE topic = :topic
159
161
  AND timestamp > :last_read_time
160
- ) a
162
+ ORDER BY timestamp ASC
163
+ LIMIT 1
164
+ """,
165
+ msg,
166
+ )
167
+ except sqlite3.OperationalError:
168
+ pass
169
+
170
+ try:
171
+ result = cursor.fetchall()[0]
172
+ message = result[0]
173
+ timestamp = result[1]
174
+ last_read_times[topic] = timestamp
175
+ except IndexError:
176
+ # Handle the case where no results are returned
177
+ message = ""
178
+
179
+ websocket.send(json.dumps({"message": message}))
180
+
181
+ elif action == "get_latest":
182
+ try:
183
+ last_read_time = last_read_times[topic]
184
+ except KeyError:
185
+ last_read_times[topic] = client_creation_time
186
+ last_read_time = last_read_times[topic]
187
+ msg["last_read_time"] = last_read_time
188
+
189
+ try:
190
+ cursor.execute(
191
+ """
192
+ SELECT message,
193
+ timestamp
194
+ FROM messages
161
195
  WHERE topic = :topic
162
- AND timestamp = a.min_time
196
+ AND timestamp > :last_read_time
197
+ ORDER BY timestamp DESC
198
+ LIMIT 1;
163
199
  """,
164
200
  msg,
165
201
  )
@@ -177,25 +213,41 @@ def request_handler(websocket):
177
213
 
178
214
  websocket.send(json.dumps({"message": message}))
179
215
 
180
- elif msg["action"] == "shutdown":
216
+ elif action == "shutdown":
181
217
  # Run this from a separate thread to prevent deadlock
182
218
  global dsmq_server
183
219
 
184
220
  def shutdown_gracefully(server_to_shutdown):
185
221
  server_to_shutdown.shutdown()
186
- # cleanup_temp_files()
187
222
 
188
223
  Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
189
224
  break
190
225
  else:
191
226
  raise RuntimeWarning(
192
- "dsmq client action must either be 'put', 'get', or 'shutdown'"
227
+ "dsmq client action must either be\n"
228
+ + "'put', 'get', 'get_wait', 'get_latest', or 'shutdown'"
193
229
  )
194
230
 
195
231
  # Periodically clean out messages to keep individual queues at
196
232
  # a manageable length and the overall mq small.
197
233
  if time.time() - time_of_last_purge > _time_between_cleanup:
234
+ cutoff_time = time.time() - _time_to_keep
198
235
  try:
236
+ cursor.execute(
237
+ """
238
+ DELETE
239
+ FROM messages
240
+ WHERE topic = :topic
241
+ AND timestamp < :cutoff_time
242
+ """,
243
+ {
244
+ "cutoff_time": cutoff_time,
245
+ "topic": topic,
246
+ },
247
+ )
248
+ sqlite_conn.commit()
249
+ time_of_last_purge = time.time()
250
+
199
251
  cursor.execute(
200
252
  """
201
253
  DELETE
@@ -219,6 +271,7 @@ def request_handler(websocket):
219
271
  )
220
272
  sqlite_conn.commit()
221
273
  time_of_last_purge = time.time()
274
+
222
275
  except sqlite3.OperationalError:
223
276
  # Database may be locked. Try again next time.
224
277
  pass
@@ -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()
@@ -1,4 +1,12 @@
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:
8
+ pass
9
+
2
10
  import time
3
11
 
4
12
  from dsmq.server import serve
@@ -11,8 +19,8 @@ verbose = False
11
19
  _pause = 0.01
12
20
  _very_long_pause = 1.0
13
21
 
14
- _n_iter = int(1e3)
15
- _n_long_char = int(1e4)
22
+ _n_iter = int(1e4)
23
+ _n_long_char = int(1e5)
16
24
 
17
25
  _short_msg = "q"
18
26
  _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.1
3
+ Version: 1.4.1
4
4
  Summary: A dead simple message queue
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -102,7 +102,8 @@ a queue before it connected.
102
102
  - A client will get the oldest message available on a requested topic.
103
103
  Queues are first-in-first-out.
104
104
 
105
- - Messages older than 600 seconds will be deleted from the queue.
105
+ - Messages older than a certain age (typically 600 seconds)
106
+ will be deleted from the queue.
106
107
 
107
108
  - Put and get operations are fairly quick--less than 100 $`\mu`$s of processing
108
109
  time plus any network latency--so it can comfortably handle requests at rates of
@@ -152,6 +153,16 @@ connected to the server.
152
153
  in the topic, or the topic doesn't yet exist,
153
154
  returns `""`.
154
155
 
156
+ ### `get_latest(topic)`
157
+
158
+ Get the *most recent* eligible message from the queue named `topic`.
159
+ All the messages older than that in the queue become ineligible and never
160
+ get seen by the client.
161
+ - `topic` (str)
162
+ - returns str, the content of the message. If there was no eligble message
163
+ in the topic, or the topic doesn't yet exist,
164
+ returns `""`.
165
+
155
166
  ### `get_wait(topic)`
156
167
 
157
168
  A variant of `get()` that retries a few times until it gets
@@ -0,0 +1,13 @@
1
+ dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
2
+ dsmq/client.py,sha256=a4_6jcF7RAC58And0OlR3vL42FgZ24q2BECS-VF4Ri4,3438
3
+ dsmq/demo.py,sha256=HCnoucawZ2CU0rGipLgsZjVUpP7232mUcugmhf_DjBQ,825
4
+ dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
5
+ dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
6
+ dsmq/server.py,sha256=PbmbW95VunRrQM24AdzX9HbqWPixt7XrAINV1yiHpxo,10210
7
+ dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ dsmq/tests/integration_test.py,sha256=zF6v_3UHt5KqveX22gEpCSLcoeOyEWLSy_gMLoytg2U,6303
9
+ dsmq/tests/performance_suite.py,sha256=g95jm0u2sa-1hm6m10aoRgN7_tynXFxU-bimy3V2wZM,5391
10
+ dsmq-1.4.1.dist-info/METADATA,sha256=LuoL1eb8_zH2bDCY7QRlKrZ3ndK0UJ1gXJKZ-4miWYc,5229
11
+ dsmq-1.4.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
+ dsmq-1.4.1.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
13
+ dsmq-1.4.1.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
2
- dsmq/client.py,sha256=ZsbuvsWX6_4-tkp7i3ixYAPEyvoebKX8qFtW38DHdXE,2569
3
- dsmq/demo.py,sha256=K53cC5kN7K4kNJlPq7c5OTIMHRCKTo9hYX2aIos57rU,542
4
- dsmq/example_get_client.py,sha256=PvAsDGEAH1kVBifLVg2rx8ZxnAZmvzVCvZq13VgpLds,301
5
- dsmq/example_put_client.py,sha256=QxDc3i7KAjjhpwxRRpI0Ke5KTNSPuBf9kkcGyTvUEaw,353
6
- dsmq/server.py,sha256=HXPH9-nSzz2Iy2UzIWqgUVbL174jq1VBzs7im9vRLqk,8268
7
- dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- dsmq/tests/integration_test.py,sha256=dLsQGCmpXv4zRb93TriccH7TbUyD9MHcLckAQqfDOK4,5980
9
- dsmq/tests/performance_suite.py,sha256=E59zB2ZvM8V5f8RxaB7p-Kehqyhrgsl0sXuy7g74BaI,5218
10
- dsmq-1.3.1.dist-info/METADATA,sha256=K6fqrudTKsXR2xlHDlTNkrjepNcsKn5kX9I5yjpzxPk,4859
11
- dsmq-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
12
- dsmq-1.3.1.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
13
- dsmq-1.3.1.dist-info/RECORD,,
File without changes