dsmq 1.2.3__tar.gz → 1.2.4__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dsmq
3
- Version: 1.2.3
3
+ Version: 1.2.4
4
4
  Summary: A dead simple message queue
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dsmq"
3
- version = "1.2.3"
3
+ version = "1.2.4"
4
4
  description = "A dead simple message queue"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -0,0 +1,212 @@
1
+ import json
2
+ import os
3
+ import sqlite3
4
+ import sys
5
+ from threading import Thread
6
+ import time
7
+ from websockets.sync.server import serve as ws_serve
8
+ from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK
9
+
10
+ _default_host = "127.0.0.1"
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
16
+
17
+ _db_name = "file::memory:?cache=shared"
18
+
19
+ # Make this global so it's easy to share
20
+ dsmq_server = None
21
+
22
+
23
+ def serve(host=_default_host, port=_default_port, verbose=False):
24
+ """
25
+ For best results, start this running in its own process and walk away.
26
+ """
27
+ # Cleanup temp files.
28
+ # Under some condition
29
+ # (which I haven't yet been able to pin down)
30
+ # a file is generated with the db name.
31
+ # If it is not removed, it gets
32
+ # treated as a SQLite db on disk,
33
+ # which dramatically slows it down,
34
+ # especially the way it's used here for
35
+ # rapid-fire one-item reads and writes.
36
+ filenames = os.listdir()
37
+ for filename in filenames:
38
+ if filename[: len(_db_name)] == _db_name:
39
+ os.remove(filename)
40
+
41
+ sqlite_conn = sqlite3.connect(_db_name)
42
+ cursor = sqlite_conn.cursor()
43
+ cursor.execute("""
44
+ CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
45
+ """)
46
+
47
+ # Making this global in scope is a way to make it available
48
+ # to the shutdown operation. It's an awkward construction,
49
+ # and a method of last resort. (If you stumble across this and
50
+ # figure out something more elegant, please submit a PR!
51
+ # or send it to me at brohrer@gmail.com,
52
+ global dsmq_server
53
+
54
+ # 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...")
61
+
62
+ sqlite_conn.close()
63
+
64
+
65
+ def request_handler(websocket):
66
+ sqlite_conn = sqlite3.connect(_db_name)
67
+ cursor = sqlite_conn.cursor()
68
+
69
+ client_creation_time = time.time()
70
+ last_read_times = {}
71
+ time_of_last_purge = time.time()
72
+
73
+ try:
74
+ for msg_text in websocket:
75
+ msg = json.loads(msg_text)
76
+ topic = msg["topic"]
77
+ timestamp = time.time()
78
+
79
+ if msg["action"] == "put":
80
+ msg["timestamp"] = timestamp
81
+
82
+ # This block allows for multiple retries if the database
83
+ # is busy.
84
+ for i_retry in range(_n_retries):
85
+ try:
86
+ cursor.execute(
87
+ """
88
+ INSERT INTO messages (timestamp, topic, message)
89
+ VALUES (:timestamp, :topic, :message)
90
+ """,
91
+ (msg),
92
+ )
93
+ sqlite_conn.commit()
94
+ except sqlite3.OperationalError:
95
+ wait_time = _first_retry * 2**i_retry
96
+ time.sleep(wait_time)
97
+ continue
98
+ break
99
+
100
+ elif msg["action"] == "get":
101
+ try:
102
+ last_read_time = last_read_times[topic]
103
+ except KeyError:
104
+ last_read_times[topic] = client_creation_time
105
+ last_read_time = last_read_times[topic]
106
+ msg["last_read_time"] = last_read_time
107
+
108
+ # This block allows for multiple retries if the database
109
+ # is busy.
110
+ for i_retry in range(_n_retries):
111
+ try:
112
+ cursor.execute(
113
+ """
114
+ SELECT message,
115
+ timestamp
116
+ FROM messages,
117
+ (
118
+ SELECT MIN(timestamp) AS min_time
119
+ FROM messages
120
+ WHERE topic = :topic
121
+ AND timestamp > :last_read_time
122
+ ) a
123
+ WHERE topic = :topic
124
+ AND timestamp = a.min_time
125
+ """,
126
+ msg,
127
+ )
128
+ except sqlite3.OperationalError:
129
+ wait_time = _first_retry * 2**i_retry
130
+ time.sleep(wait_time)
131
+ continue
132
+ break
133
+
134
+ try:
135
+ result = cursor.fetchall()[0]
136
+ message = result[0]
137
+ timestamp = result[1]
138
+ last_read_times[topic] = timestamp
139
+ except IndexError:
140
+ # Handle the case where no results are returned
141
+ message = ""
142
+
143
+ websocket.send(json.dumps({"message": message}))
144
+ elif msg["action"] == "shutdown":
145
+ # Run this from a separate thread to prevent deadlock
146
+ global dsmq_server
147
+
148
+ def shutdown_gracefully(server_to_shutdown):
149
+ server_to_shutdown.shutdown()
150
+
151
+ filenames = os.listdir()
152
+ for filename in filenames:
153
+ if filename[: len(_db_name)] == _db_name:
154
+ try:
155
+ os.remove(filename)
156
+ except FileNotFoundError:
157
+ pass
158
+
159
+ Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
160
+ break
161
+ else:
162
+ raise RuntimeWarning(
163
+ "dsmq client action must either be 'put', 'get', or 'shutdown'"
164
+ )
165
+
166
+ # Periodically clean out messages from the queue that are
167
+ # past their sell buy date.
168
+ # This operation is pretty fast. I clock it at 12 us on my machine.
169
+ if time.time() - time_of_last_purge > _time_to_live:
170
+ try:
171
+ cursor.execute(
172
+ """
173
+ DELETE FROM messages
174
+ WHERE timestamp < :time_threshold
175
+ """,
176
+ {"time_threshold": time_of_last_purge},
177
+ )
178
+ sqlite_conn.commit()
179
+ time_of_last_purge = time.time()
180
+ except sqlite3.OperationalError:
181
+ # Database may be locked. Try again next time.
182
+ pass
183
+ except (ConnectionClosedError, ConnectionClosedOK):
184
+ # Something happened on the other end and this handler
185
+ # is no longer needed.
186
+ pass
187
+
188
+ sqlite_conn.close()
189
+
190
+
191
+ if __name__ == "__main__":
192
+ if len(sys.argv) == 3:
193
+ host = sys.argv[1]
194
+ port = int(sys.argv[2])
195
+ serve(host=host, port=port)
196
+ elif len(sys.argv) == 2:
197
+ host = sys.argv[1]
198
+ serve(host=host)
199
+ elif len(sys.argv) == 1:
200
+ serve()
201
+ else:
202
+ print(
203
+ """
204
+ Try one of these:
205
+ $ python3 server.py
206
+
207
+ $ python3 server.py 127.0.0.1
208
+
209
+ $ python3 server.py 127.0.0.1 25853
210
+
211
+ """
212
+ )
@@ -1,6 +1,5 @@
1
1
  import multiprocessing as mp
2
2
  import time
3
- from websockets.exceptions import ConnectionClosed
4
3
  from dsmq.server import serve
5
4
  from dsmq.client import connect
6
5
 
@@ -12,7 +12,7 @@ wheels = [
12
12
 
13
13
  [[package]]
14
14
  name = "dsmq"
15
- version = "1.2.2"
15
+ version = "1.2.4"
16
16
  source = { editable = "." }
17
17
  dependencies = [
18
18
  { name = "pytest" },
@@ -1,206 +0,0 @@
1
- import json
2
- import os
3
- import sqlite3
4
- import sys
5
- from threading import Thread
6
- import time
7
- from websockets.sync.server import serve as ws_serve
8
- from websockets.exceptions import ConnectionClosedError
9
-
10
- _default_host = "127.0.0.1"
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
16
-
17
- _db_name = "file::memory:?cache=shared"
18
-
19
- # Make this global so it's easy to share
20
- dsmq_server = None
21
-
22
-
23
- def serve(host=_default_host, port=_default_port, verbose=False):
24
- """
25
- For best results, start this running in its own process and walk away.
26
- """
27
- # Cleanup temp files.
28
- # Under some condition
29
- # (which I haven't yet been able to pin down)
30
- # a file is generated with the db name.
31
- # If it is not removed, it gets
32
- # treated as a SQLite db on disk,
33
- # which dramatically slows it down,
34
- # especially the way it's used here for
35
- # rapid-fire one-item reads and writes.
36
- filenames = os.listdir()
37
- for filename in filenames:
38
- if filename[: len(_db_name)] == _db_name:
39
- os.remove(filename)
40
-
41
- sqlite_conn = sqlite3.connect(_db_name)
42
- cursor = sqlite_conn.cursor()
43
- cursor.execute("""
44
- CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
45
- """)
46
-
47
- # Making this global in scope is a way to make it available
48
- # to the shutdown operation. It's an awkward construction,
49
- # and a method of last resort. (If you stumble across this and
50
- # figure out something more elegant, please submit a PR!
51
- # or send it to me at brohrer@gmail.com,
52
- global dsmq_server
53
-
54
- # 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...")
61
-
62
- sqlite_conn.close()
63
-
64
-
65
- def request_handler(websocket):
66
- sqlite_conn = sqlite3.connect(_db_name)
67
- cursor = sqlite_conn.cursor()
68
-
69
- client_creation_time = time.time()
70
- last_read_times = {}
71
- time_of_last_purge = time.time()
72
-
73
- for msg_text in websocket:
74
- msg = json.loads(msg_text)
75
- topic = msg["topic"]
76
- timestamp = time.time()
77
-
78
- if msg["action"] == "put":
79
- msg["timestamp"] = timestamp
80
-
81
- # This block allows for multiple retries if the database
82
- # is busy.
83
- for i_retry in range(_n_retries):
84
- try:
85
- cursor.execute(
86
- """
87
- INSERT INTO messages (timestamp, topic, message)
88
- VALUES (:timestamp, :topic, :message)
89
- """,
90
- (msg),
91
- )
92
- sqlite_conn.commit()
93
- except sqlite3.OperationalError:
94
- wait_time = _first_retry * 2**i_retry
95
- time.sleep(wait_time)
96
- continue
97
- break
98
-
99
- elif msg["action"] == "get":
100
- try:
101
- last_read_time = last_read_times[topic]
102
- except KeyError:
103
- last_read_times[topic] = client_creation_time
104
- last_read_time = last_read_times[topic]
105
- msg["last_read_time"] = last_read_time
106
-
107
- # This block allows for multiple retries if the database
108
- # is busy.
109
- for i_retry in range(_n_retries):
110
- try:
111
- cursor.execute(
112
- """
113
- SELECT message,
114
- timestamp
115
- FROM messages,
116
- (
117
- SELECT MIN(timestamp) AS min_time
118
- FROM messages
119
- WHERE topic = :topic
120
- AND timestamp > :last_read_time
121
- ) a
122
- WHERE topic = :topic
123
- AND timestamp = a.min_time
124
- """,
125
- msg,
126
- )
127
- except sqlite3.OperationalError:
128
- wait_time = _first_retry * 2**i_retry
129
- time.sleep(wait_time)
130
- continue
131
- break
132
-
133
- try:
134
- result = cursor.fetchall()[0]
135
- message = result[0]
136
- timestamp = result[1]
137
- last_read_times[topic] = timestamp
138
- except IndexError:
139
- # Handle the case where no results are returned
140
- message = ""
141
-
142
- try:
143
- websocket.send(json.dumps({"message": message}))
144
- except ConnectionClosedError:
145
- pass
146
- elif msg["action"] == "shutdown":
147
- # Run this from a separate thread to prevent deadlock
148
- global dsmq_server
149
-
150
- def shutdown_gracefully(server_to_shutdown):
151
- server_to_shutdown.shutdown()
152
-
153
- filenames = os.listdir()
154
- for filename in filenames:
155
- if filename[: len(_db_name)] == _db_name:
156
- try:
157
- os.remove(filename)
158
- except FileNotFoundError:
159
- pass
160
-
161
- Thread(target=shutdown_gracefully, args=(dsmq_server,)).start()
162
- break
163
- else:
164
- raise RuntimeWarning(
165
- "dsmq client action must either be 'put', 'get', or 'shutdown'"
166
- )
167
-
168
- # Periodically clean out messages from the queue that are
169
- # past their sell buy date.
170
- # This operation is pretty fast. I clock it at 12 us on my machine.
171
- if time.time() - time_of_last_purge > _time_to_live:
172
- cursor.execute(
173
- """
174
- DELETE FROM messages
175
- WHERE timestamp < :time_threshold
176
- """,
177
- {"time_threshold": time_of_last_purge},
178
- )
179
- sqlite_conn.commit()
180
- time_of_last_purge = time.time()
181
-
182
- sqlite_conn.close()
183
-
184
-
185
- if __name__ == "__main__":
186
- if len(sys.argv) == 3:
187
- host = sys.argv[1]
188
- port = int(sys.argv[2])
189
- serve(host=host, port=port)
190
- elif len(sys.argv) == 2:
191
- host = sys.argv[1]
192
- serve(host=host)
193
- elif len(sys.argv) == 1:
194
- serve()
195
- else:
196
- print(
197
- """
198
- Try one of these:
199
- $ python3 server.py
200
-
201
- $ python3 server.py 127.0.0.1
202
-
203
- $ python3 server.py 127.0.0.1 25853
204
-
205
- """
206
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes