dsmq 1.2.4__py3-none-any.whl → 1.3.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/.server.py.swp ADDED
Binary file
dsmq/client.py CHANGED
@@ -8,6 +8,7 @@ _default_port = 30008
8
8
 
9
9
  _n_retries = 10
10
10
  _initial_retry = 0.01 # seconds
11
+ _shutdown_delay = 0.1 # seconds
11
12
 
12
13
 
13
14
  def connect(host=_default_host, port=_default_port, verbose=False):
@@ -68,6 +69,10 @@ class DSMQClientSideConnection:
68
69
  def shutdown_server(self):
69
70
  msg_dict = {"action": "shutdown", "topic": ""}
70
71
  self.websocket.send(json.dumps(msg_dict))
72
+ # Give the server time to wind down
73
+ time.sleep(_shutdown_delay)
71
74
 
72
75
  def close(self):
73
76
  self.websocket.close()
77
+ # Give the websocket time to wind down
78
+ time.sleep(_shutdown_delay)
dsmq/server.py CHANGED
@@ -9,12 +9,16 @@ from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
9
9
 
10
10
  _default_host = "127.0.0.1"
11
11
  _default_port = 30008
12
- _n_retries = 5
13
- _first_retry = 0.01 # seconds
14
- _pause = 0.01 # seconds
15
- _time_to_live = 600.0 # seconds
12
+ _n_retries = 20
13
+ _first_retry = 0.005 # seconds
14
+ _time_to_live = 60.0 # seconds
16
15
 
16
+ # _db_name = ":memory:"
17
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."
18
22
 
19
23
  # Make this global so it's easy to share
20
24
  dsmq_server = None
@@ -40,6 +44,14 @@ def serve(host=_default_host, port=_default_port, verbose=False):
40
44
 
41
45
  sqlite_conn = sqlite3.connect(_db_name)
42
46
  cursor = sqlite_conn.cursor()
47
+
48
+ # Tweak the connection to make it faster
49
+ # and keep long-term latency more predictable.
50
+ # cursor.execute("PRAGMA journal_mode = OFF")
51
+ cursor.execute("PRAGMA journal_mode = WAL")
52
+ # cursor.execute("PRAGMA synchronous = OFF")
53
+ # cursor.execute("PRAGMA secure_delete = OFF")
54
+
43
55
  cursor.execute("""
44
56
  CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
45
57
  """)
@@ -52,12 +64,33 @@ CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
52
64
  global dsmq_server
53
65
 
54
66
  # dsmq_server = ws_serve(request_handler, host, port)
55
- with ws_serve(request_handler, host, port) as dsmq_server:
56
- dsmq_server.serve_forever()
57
- if verbose:
58
- print()
59
- print(f"Server started at {host} on port {port}.")
60
- print("Waiting for clients...")
67
+ for i_retry in range(_n_retries):
68
+ try:
69
+ with ws_serve(request_handler, host, port) as dsmq_server:
70
+ dsmq_server.serve_forever()
71
+
72
+ if verbose:
73
+ print()
74
+ print(f"Server started at {host} on port {port}.")
75
+ print("Waiting for clients...")
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)
61
94
 
62
95
  sqlite_conn.close()
63
96
 
Binary file
@@ -0,0 +1,179 @@
1
+ import multiprocessing as mp
2
+ import time
3
+
4
+ from dsmq.server import serve
5
+ from dsmq.client import connect
6
+
7
+ host = "127.0.0.1"
8
+ port = 30303
9
+ verbose = False
10
+
11
+ _pause = 0.01
12
+ _very_long_pause = 1.0
13
+
14
+ _n_iter = int(1e3)
15
+ _n_long_char = int(1e4)
16
+
17
+ _short_msg = "q"
18
+ _long_msg = str(["q"] * _n_long_char)
19
+
20
+ _test_topic = "test"
21
+
22
+
23
+ def main():
24
+ print()
25
+ print("dsmq timing measurements")
26
+
27
+ time_short_writes()
28
+ time_long_writes()
29
+ time_empty_reads()
30
+ time_short_reads()
31
+ time_long_reads()
32
+
33
+
34
+ def time_short_writes():
35
+ condition = "short write"
36
+
37
+ duration, duration_close = time_writes(msg=_short_msg, n_iter=1)
38
+
39
+ print()
40
+ print(f"Time for first {condition} [including closing]")
41
+ print(f" {int(duration)} μs [{int(duration_close)} μs]")
42
+
43
+ avg_duration, avg_duration_close = time_writes(msg=_short_msg, n_iter=_n_iter)
44
+
45
+ print(f"Average time for a {condition} [including closing]")
46
+ print(f" {int(avg_duration)} μs [{int(avg_duration_close)} μs]")
47
+
48
+
49
+ def time_long_writes():
50
+ duration, duration_close = time_writes(msg=_long_msg, n_iter=1)
51
+
52
+ condition = "long write"
53
+ print()
54
+ print(f"Time for first {condition} [including closing]")
55
+ print(f" {int(duration)} μs [{int(duration_close)} μs]")
56
+
57
+ avg_duration, avg_duration_close = time_writes(msg=_long_msg, n_iter=_n_iter)
58
+
59
+ condition = f"long write ({_n_long_char} characters)"
60
+ print(f"Average time for a {condition} [including closing]")
61
+ print(f" {int(avg_duration)} μs [{int(avg_duration_close)} μs]")
62
+
63
+ condition = "long write (per 1000 characters)"
64
+ print(f"Average time for a {condition} [including closing]")
65
+ print(
66
+ f" {int(1000 * avg_duration / _n_long_char)} μs "
67
+ + f"[{int(1000 * avg_duration_close / _n_long_char)}] μs"
68
+ )
69
+
70
+
71
+ def time_writes(msg="message", n_iter=1):
72
+ p_server = mp.Process(target=serve, args=(host, port, verbose))
73
+ p_server.start()
74
+ time.sleep(_pause)
75
+ write_client = connect(host, port)
76
+
77
+ start_time = time.time()
78
+ for _ in range(n_iter):
79
+ write_client.put(_test_topic, msg)
80
+ avg_duration = 1e6 * (time.time() - start_time) / n_iter # microseconds
81
+
82
+ write_client.shutdown_server()
83
+ write_client.close()
84
+
85
+ p_server.join(_very_long_pause)
86
+ if p_server.is_alive():
87
+ print(" Doing a hard shutdown on mq server")
88
+ p_server.kill()
89
+ avg_duration_close = 1e6 * (time.time() - start_time) / n_iter # microseconds
90
+
91
+ return avg_duration, avg_duration_close
92
+
93
+
94
+ def time_empty_reads():
95
+ condition = "empty read"
96
+
97
+ duration, duration_close = time_reads(msg=None, n_iter=1)
98
+
99
+ print()
100
+ print(f"Time for first {condition} [including closing]")
101
+ print(f" {int(duration)} μs [{int(duration_close)} μs]")
102
+
103
+ avg_duration, avg_duration_close = time_reads(msg=None, n_iter=_n_iter)
104
+
105
+ print(f"Average time for a {condition} [including closing]")
106
+ print(f" {int(avg_duration)} μs [{int(avg_duration_close)} μs]")
107
+
108
+
109
+ def time_short_reads():
110
+ condition = "short read"
111
+
112
+ duration, duration_close = time_reads(msg=_short_msg, n_iter=1)
113
+
114
+ print()
115
+ print(f"Time for first {condition} [including closing]")
116
+ print(f" {int(duration)} μs [{int(duration_close)} μs]")
117
+
118
+ avg_duration, avg_duration_close = time_reads(msg=_short_msg, n_iter=_n_iter)
119
+
120
+ print(f"Average time for a {condition} [including closing]")
121
+ print(f" {int(avg_duration)} μs [{int(avg_duration_close)} μs]")
122
+
123
+
124
+ def time_long_reads():
125
+ condition = f"long read ({_n_long_char} characters)"
126
+
127
+ duration, duration_close = time_reads(msg=_long_msg, n_iter=1)
128
+
129
+ print()
130
+ print(f"Time for first {condition} [including closing]")
131
+ print(f" {int(duration)} μs [{int(duration_close)} μs]")
132
+
133
+ avg_duration, avg_duration_close = time_reads(msg=_long_msg, n_iter=_n_iter)
134
+
135
+ print(f"Average time for a {condition} [including closing]")
136
+ print(f" {int(avg_duration)} μs [{int(avg_duration_close)} μs]")
137
+
138
+ condition = "long read (per 1000 characters)"
139
+ print(f"Average time for a {condition} [including closing]")
140
+ print(
141
+ f" {int(1000 * avg_duration / _n_long_char)} μs "
142
+ + f"[{int(1000 * avg_duration_close / _n_long_char)}] μs"
143
+ )
144
+
145
+
146
+ def time_reads(msg=None, n_iter=1):
147
+ p_server = mp.Process(target=serve, args=(host, port, verbose))
148
+ p_server.start()
149
+ time.sleep(_pause)
150
+ # write_client = connect(host, port)
151
+ read_client = connect(host, port)
152
+
153
+ if msg is not None:
154
+ for _ in range(n_iter):
155
+ read_client.put(_test_topic, msg)
156
+
157
+ start_time = time.time()
158
+ for _ in range(n_iter):
159
+ msg = read_client.get(_test_topic)
160
+
161
+ avg_duration = 1e6 * (time.time() - start_time) / n_iter # microseconds
162
+
163
+ read_client.shutdown_server()
164
+ # write_client.close()
165
+ read_client.close()
166
+
167
+ p_server.join(_very_long_pause)
168
+
169
+ if p_server.is_alive():
170
+ print(" Doing a hard shutdown on mq server")
171
+ p_server.kill()
172
+
173
+ avg_duration_close = 1e6 * (time.time() - start_time) / n_iter # microseconds
174
+
175
+ return avg_duration, avg_duration_close
176
+
177
+
178
+ if __name__ == "__main__":
179
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsmq
3
- Version: 1.2.4
3
+ Version: 1.3.0
4
4
  Summary: A dead simple message queue
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -178,3 +178,8 @@ Run all the tests in `src/dsmq/tests/` with pytest, for example
178
178
  ```
179
179
  uv run pytest
180
180
  ```
181
+
182
+ # Performance characterization
183
+
184
+ Time typical operations on your system with the script at
185
+ `src/dsmq/tests/performance_suite.py`
@@ -0,0 +1,15 @@
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,,
@@ -1,12 +0,0 @@
1
- dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
2
- dsmq/client.py,sha256=p6irQZOE4b2fpTUwSrEUraPmrJzvT8QSU01ak9qpGCQ,2351
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=5c4mHWz6yslSxpOwrZ3vOCMcx_cm8zMF8iJNnQF8ft0,7249
7
- dsmq/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- dsmq/tests/integration_test.py,sha256=dLsQGCmpXv4zRb93TriccH7TbUyD9MHcLckAQqfDOK4,5980
9
- dsmq-1.2.4.dist-info/METADATA,sha256=CswX8bQph2pF-k-VMCLAPqaho3Mhny4xDLJ70xwWa9I,4730
10
- dsmq-1.2.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- dsmq-1.2.4.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
12
- dsmq-1.2.4.dist-info/RECORD,,
File without changes