dsmq 0.2.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/__init__.py +2 -0
- dsmq/demo_linux.py +25 -0
- dsmq/dsmq.py +160 -0
- dsmq/example_get_client.py +22 -0
- dsmq/example_put_client.py +19 -0
- dsmq/example_server.py +3 -0
- dsmq/py.typed +0 -0
- dsmq-0.2.0.dist-info/METADATA +156 -0
- dsmq-0.2.0.dist-info/RECORD +11 -0
- dsmq-0.2.0.dist-info/WHEEL +4 -0
- dsmq-0.2.0.dist-info/licenses/LICENSE +21 -0
dsmq/__init__.py
ADDED
dsmq/demo_linux.py
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# Heads up: This script only works on Linux because of use of "fork" method.
|
2
|
+
import multiprocessing as mp
|
3
|
+
import dsmq
|
4
|
+
import example_get_client
|
5
|
+
import example_put_client
|
6
|
+
|
7
|
+
mp.set_start_method("fork")
|
8
|
+
|
9
|
+
HOST = "127.0.0.1"
|
10
|
+
PORT = 25252
|
11
|
+
|
12
|
+
|
13
|
+
def test_server_with_clients():
|
14
|
+
p_server = mp.Process(target=dsmq.run, args=(HOST, PORT))
|
15
|
+
p_server.start()
|
16
|
+
|
17
|
+
p_putter = mp.Process(target=example_put_client.run, args=(HOST, PORT, 20))
|
18
|
+
p_getter = mp.Process(target=example_get_client.run, args=(HOST, PORT, 20))
|
19
|
+
|
20
|
+
p_putter.start()
|
21
|
+
p_getter.start()
|
22
|
+
|
23
|
+
|
24
|
+
if __name__ == "__main__":
|
25
|
+
test_server_with_clients()
|
dsmq/dsmq.py
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
import json
|
2
|
+
import socket
|
3
|
+
import sqlite3
|
4
|
+
import sys
|
5
|
+
from threading import Thread
|
6
|
+
import time
|
7
|
+
|
8
|
+
N_RETRIES = 5
|
9
|
+
FIRST_RETRY = 0.01 # seconds
|
10
|
+
|
11
|
+
|
12
|
+
def run(host="127.0.0.1", port=30008):
|
13
|
+
sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
|
14
|
+
cursor = sqlite_conn.cursor()
|
15
|
+
|
16
|
+
cursor.execute("""
|
17
|
+
CREATE TABLE IF NOT EXISTS messages (timestamp DOUBLE, topic TEXT, message TEXT)
|
18
|
+
""")
|
19
|
+
|
20
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
21
|
+
# Setting this socket option to re-use the address,
|
22
|
+
# even if it's already in use.
|
23
|
+
# This is helpful in recovering from crashes where things didn't
|
24
|
+
# shut down properly.
|
25
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
26
|
+
|
27
|
+
s.bind((host, port))
|
28
|
+
s.listen()
|
29
|
+
|
30
|
+
print()
|
31
|
+
print(f"Server started at {host} on port {port}.")
|
32
|
+
print("Waiting for clients...")
|
33
|
+
|
34
|
+
while True:
|
35
|
+
socket_conn, addr = s.accept()
|
36
|
+
print(f"Connected by {addr}")
|
37
|
+
Thread(target=handle_socket, args=(socket_conn,)).start()
|
38
|
+
|
39
|
+
sqlite_conn.close()
|
40
|
+
|
41
|
+
|
42
|
+
def handle_socket(socket_conn):
|
43
|
+
sqlite_conn = sqlite3.connect("file:mem1?mode=memory&cache=shared")
|
44
|
+
cursor = sqlite_conn.cursor()
|
45
|
+
|
46
|
+
creation_time = time.time()
|
47
|
+
last_read = {}
|
48
|
+
|
49
|
+
while True:
|
50
|
+
data = socket_conn.recv(1024)
|
51
|
+
# Check whether the connection has been terminated
|
52
|
+
if not data:
|
53
|
+
break
|
54
|
+
|
55
|
+
msg_str = data.decode("utf-8")
|
56
|
+
try:
|
57
|
+
# print("dsmq received ", msg_str)
|
58
|
+
msg = json.loads(msg_str)
|
59
|
+
except json.decoder.JSONDecodeError:
|
60
|
+
print("Message must be json-friendly")
|
61
|
+
print(f" Received: {msg}")
|
62
|
+
continue
|
63
|
+
|
64
|
+
topic = msg["topic"]
|
65
|
+
timestamp = time.time()
|
66
|
+
|
67
|
+
if msg["action"] == "put":
|
68
|
+
msg["timestamp"] = timestamp
|
69
|
+
|
70
|
+
# This block allows for multiple retries if the database
|
71
|
+
# is busy.
|
72
|
+
for i_retry in range(N_RETRIES):
|
73
|
+
try:
|
74
|
+
cursor.execute(
|
75
|
+
"""
|
76
|
+
INSERT INTO messages (timestamp, topic, message)
|
77
|
+
VALUES (:timestamp, :topic, :message)
|
78
|
+
""",
|
79
|
+
(msg),
|
80
|
+
)
|
81
|
+
sqlite_conn.commit()
|
82
|
+
except sqlite3.OperationalError:
|
83
|
+
wait_time = FIRST_RETRY * 2**i_retry
|
84
|
+
time.sleep(wait_time)
|
85
|
+
continue
|
86
|
+
break
|
87
|
+
|
88
|
+
elif msg["action"] == "get":
|
89
|
+
try:
|
90
|
+
last_read_time = last_read[topic]
|
91
|
+
except KeyError:
|
92
|
+
last_read[topic] = creation_time
|
93
|
+
last_read_time = last_read[topic]
|
94
|
+
msg["last_read_time"] = last_read_time
|
95
|
+
|
96
|
+
# This block allows for multiple retries if the database
|
97
|
+
# is busy.
|
98
|
+
for i_retry in range(N_RETRIES):
|
99
|
+
try:
|
100
|
+
cursor.execute(
|
101
|
+
"""
|
102
|
+
SELECT message,
|
103
|
+
timestamp
|
104
|
+
FROM messages,
|
105
|
+
(
|
106
|
+
SELECT MIN(timestamp) AS min_time
|
107
|
+
FROM messages
|
108
|
+
WHERE topic = :topic
|
109
|
+
AND timestamp > :last_read_time
|
110
|
+
) a
|
111
|
+
WHERE topic = :topic
|
112
|
+
AND timestamp = a.min_time
|
113
|
+
""",
|
114
|
+
msg,
|
115
|
+
)
|
116
|
+
except sqlite3.OperationalError:
|
117
|
+
wait_time = FIRST_RETRY * 2**i_retry
|
118
|
+
time.sleep(wait_time)
|
119
|
+
continue
|
120
|
+
break
|
121
|
+
|
122
|
+
try:
|
123
|
+
result = cursor.fetchall()[0]
|
124
|
+
message = result[0]
|
125
|
+
timestamp = result[1]
|
126
|
+
last_read[topic] = timestamp
|
127
|
+
except IndexError:
|
128
|
+
# Handle the case where no results are returned
|
129
|
+
message = ""
|
130
|
+
|
131
|
+
msg = json.dumps({"message": message})
|
132
|
+
socket_conn.sendall(bytes(msg, "utf-8"))
|
133
|
+
else:
|
134
|
+
print("Action must either be 'put' or 'get'")
|
135
|
+
|
136
|
+
sqlite_conn.close()
|
137
|
+
|
138
|
+
|
139
|
+
if __name__ == "__main__":
|
140
|
+
if len(sys.argv) == 3:
|
141
|
+
host = sys.argv[1]
|
142
|
+
port = int(sys.argv[2])
|
143
|
+
run(host=host, port=port)
|
144
|
+
elif len(sys.argv) == 2:
|
145
|
+
host = sys.argv[1]
|
146
|
+
run(host=host)
|
147
|
+
elif len(sys.argv) == 1:
|
148
|
+
run()
|
149
|
+
else:
|
150
|
+
print(
|
151
|
+
"""
|
152
|
+
Try one of these:
|
153
|
+
$ python3 dsmq.py
|
154
|
+
|
155
|
+
$ python3 dsmq.py 127.0.0.1
|
156
|
+
|
157
|
+
$ python3 dsmq.py 127.0.0.1 25853
|
158
|
+
|
159
|
+
"""
|
160
|
+
)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import json
|
2
|
+
import socket
|
3
|
+
import time
|
4
|
+
|
5
|
+
|
6
|
+
def run(host="127.0.0.1", port=30008, n_iter=1000):
|
7
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
8
|
+
s.connect((host, port))
|
9
|
+
|
10
|
+
for i in range(n_iter):
|
11
|
+
time.sleep(1)
|
12
|
+
msg = json.dumps({"action": "get", "topic": "greetings"})
|
13
|
+
s.sendall(bytes(msg, "utf-8"))
|
14
|
+
data = s.recv(1024)
|
15
|
+
if not data:
|
16
|
+
raise RuntimeError("Connection terminated by server")
|
17
|
+
msg_str = data.decode("utf-8")
|
18
|
+
print(f"client received {json.loads(msg_str)}")
|
19
|
+
|
20
|
+
|
21
|
+
if __name__ == "__main__":
|
22
|
+
run()
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import json
|
2
|
+
import socket
|
3
|
+
import time
|
4
|
+
|
5
|
+
|
6
|
+
def run(host="127.0.0.1", port=30008, n_iter=1000):
|
7
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
8
|
+
s.connect((host, port))
|
9
|
+
|
10
|
+
for i in range(n_iter):
|
11
|
+
time.sleep(1)
|
12
|
+
note = f"{i}. Hello, world"
|
13
|
+
msg = json.dumps({"action": "put", "topic": "greetings", "message": note})
|
14
|
+
s.sendall(bytes(msg, "utf-8"))
|
15
|
+
print(f"client sent {msg}")
|
16
|
+
|
17
|
+
|
18
|
+
if __name__ == "__main__":
|
19
|
+
run()
|
dsmq/example_server.py
ADDED
dsmq/py.typed
ADDED
File without changes
|
@@ -0,0 +1,156 @@
|
|
1
|
+
Metadata-Version: 2.3
|
2
|
+
Name: dsmq
|
3
|
+
Version: 0.2.0
|
4
|
+
Summary: A dead simple message queue
|
5
|
+
License-File: LICENSE
|
6
|
+
Requires-Python: >=3.10
|
7
|
+
Description-Content-Type: text/markdown
|
8
|
+
|
9
|
+
# Dead Simple Message Queue
|
10
|
+
|
11
|
+
## What it does
|
12
|
+
|
13
|
+
Part mail room, part bulletin board, dsmq is a central location for sharing messages
|
14
|
+
between processes, even when they are running on computers scattered around the world.
|
15
|
+
|
16
|
+
Its defining characteristic is its bare-bones simplicity.
|
17
|
+
|
18
|
+
## How to use it
|
19
|
+
|
20
|
+
### Install
|
21
|
+
|
22
|
+
```bash
|
23
|
+
pip install dsmq
|
24
|
+
```
|
25
|
+
### Create a dsmq server
|
26
|
+
|
27
|
+
As in `src/dsmq/example_server.py`
|
28
|
+
|
29
|
+
```python
|
30
|
+
from dsmq import dsmq
|
31
|
+
|
32
|
+
dsmq.run(host="127.0.0.1", port=12345)
|
33
|
+
```
|
34
|
+
|
35
|
+
### Add a message to a queue
|
36
|
+
|
37
|
+
As in `src/dsmq/example_put_client.py`
|
38
|
+
|
39
|
+
```python
|
40
|
+
import json
|
41
|
+
import socket
|
42
|
+
|
43
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
44
|
+
s.connect(("127.0.0.1", 12345))
|
45
|
+
message_content = {"action": "put", "topic": "greetings", "message": "Hello!"}
|
46
|
+
msg = json.dumps(message_content)
|
47
|
+
s.sendall(bytes(msg, "utf-8"))
|
48
|
+
```
|
49
|
+
|
50
|
+
### Read a message from a queue
|
51
|
+
|
52
|
+
As in `src/dsmq/example_get_client.py`
|
53
|
+
|
54
|
+
```python
|
55
|
+
import json
|
56
|
+
import socket
|
57
|
+
|
58
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
59
|
+
s.connect(("127.0.0.1", 12345))
|
60
|
+
|
61
|
+
for i in range(n_iter):
|
62
|
+
request_message_content = {"action": "get", "topic": "greetings"}
|
63
|
+
request_msg = json.dumps(request_message_content)
|
64
|
+
s.sendall(bytes(reequest_msg, "utf-8"))
|
65
|
+
|
66
|
+
reply_msg = s.recv(1024)
|
67
|
+
if not reply_msg:
|
68
|
+
raise RuntimeError("Connection terminated by server")
|
69
|
+
reply_msg_content = reply_msg.decode("utf-8")
|
70
|
+
```
|
71
|
+
|
72
|
+
### Demo
|
73
|
+
|
74
|
+
1. Open 3 separate terminal windows.
|
75
|
+
1. In the first, run `src/dsmq/dsmq.py`.
|
76
|
+
1. In the second, run `src/dsmq/example_put_client.py`.
|
77
|
+
1. In the third, run `src/dsmq/example_get_client.py`.
|
78
|
+
|
79
|
+
|
80
|
+
## How it works
|
81
|
+
|
82
|
+
### Expected behavior and limitations
|
83
|
+
|
84
|
+
- Many clients can read messages of the same topic. It is a one-to-many
|
85
|
+
pulication model.
|
86
|
+
|
87
|
+
- A client will not be able to read any of the messages that were put into
|
88
|
+
a queue before it connected.
|
89
|
+
|
90
|
+
- A client will get the oldest message available on a requested topic.
|
91
|
+
Queues are first-in-first-out.
|
92
|
+
|
93
|
+
- Put and get operations are fairly quick--less than 100 $`\mu`$s of processing
|
94
|
+
time plus any network latency--so it can comfortably handle operations at
|
95
|
+
hundreds of Hz. But if you try to have several clients reading and writing
|
96
|
+
at 1 kHz or more, you may overload the queue.
|
97
|
+
|
98
|
+
- The queue is backed by an in-memory SQLite database. If your message volumes
|
99
|
+
get larger than your RAM, you may reach an out-of-memory condition.
|
100
|
+
|
101
|
+
|
102
|
+
# API Reference and Cookbook
|
103
|
+
[[source](https://github.com/brohrer/dsmq/blob/main/src/dsmq/dsmq.py)]
|
104
|
+
|
105
|
+
### Start a server
|
106
|
+
|
107
|
+
```python
|
108
|
+
run(host="127.0.0.1", port=30008)
|
109
|
+
```
|
110
|
+
|
111
|
+
Kicks off the mesage queue server. This process will be the central exchange
|
112
|
+
for all incoming and outgoing messages.
|
113
|
+
- `host` (str), IP address on which the server will be visible and
|
114
|
+
- `port` (int), port. These will be used by all clients.
|
115
|
+
Non-privileged ports are numbered 1024 and higher.
|
116
|
+
|
117
|
+
### Open a connection from a client
|
118
|
+
|
119
|
+
```python
|
120
|
+
import socket
|
121
|
+
|
122
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
123
|
+
s.connect((host, port ))
|
124
|
+
```
|
125
|
+
|
126
|
+
### Add a message to a queue
|
127
|
+
|
128
|
+
```python
|
129
|
+
import json
|
130
|
+
msg = json.dumps({
|
131
|
+
"action": "put",
|
132
|
+
"topic": <queue-name>,
|
133
|
+
"message": <message-content>
|
134
|
+
})
|
135
|
+
s.sendall(bytes(msg, "utf-8"))
|
136
|
+
```
|
137
|
+
|
138
|
+
- `s`, the socket connection to the server
|
139
|
+
- `<queue-name>` (str), a name for the queue where the message will be added
|
140
|
+
- `<message-content>` (str), whatever message content you want
|
141
|
+
|
142
|
+
Place `message-content` into the queue named `queue-name`.
|
143
|
+
If the queue doesn't exist yet, create it.
|
144
|
+
|
145
|
+
### Get a message from a queue
|
146
|
+
|
147
|
+
```python
|
148
|
+
request_msg = json.dumps({"action": "get", "topic": <queue-name>})
|
149
|
+
s.sendall(bytes(request_msg, "utf-8"))
|
150
|
+
data = s.recv(1024)
|
151
|
+
msg_str = data.decode("utf-8")
|
152
|
+
```
|
153
|
+
|
154
|
+
Get the oldest eligible message from the queue named `<queue-name>`.
|
155
|
+
The client is only elgibile to receive messages added after it
|
156
|
+
connected to the server.
|
@@ -0,0 +1,11 @@
|
|
1
|
+
dsmq/__init__.py,sha256=YCgbnQAk8YbtHRyMcU0v2O7RdRhPhlT-vS_q40a7Q6g,50
|
2
|
+
dsmq/demo_linux.py,sha256=D5NjEvPG-fTGRRbYzMJXgtwfrigOt_eaR6D0AozBNbA,613
|
3
|
+
dsmq/dsmq.py,sha256=EJW6tMeG57QYBneqBEM4R_5CrtG-Lx2qz0D1BM0YtjI,4452
|
4
|
+
dsmq/example_get_client.py,sha256=ZWZrAu7sIEn8VwCK09cTwsm1XnF3Jkor3r7Gl4ljUMw,630
|
5
|
+
dsmq/example_put_client.py,sha256=bSjSJZsFZB6inVGUlTnJn0jgrpVNXvrN46STTvgwCzs,497
|
6
|
+
dsmq/example_server.py,sha256=UM69GN7uORDetXQM8LlW68nrA-1i7Jgp2dZREVEqxaU,62
|
7
|
+
dsmq/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
dsmq-0.2.0.dist-info/METADATA,sha256=TW6tM_oF2LuyRrmOBSBTHFCkJvNDA8O3QfuEtgD1-S8,4055
|
9
|
+
dsmq-0.2.0.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
|
10
|
+
dsmq-0.2.0.dist-info/licenses/LICENSE,sha256=3Yu1mAp5VsKmnDtzkiOY7BdmrLeNwwZ3t6iWaLnlL0Y,1071
|
11
|
+
dsmq-0.2.0.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 Brandon Rohrer
|
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.
|