pulsar-lite 0.1.0__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.
- pulsar_lite-0.1.0/PKG-INFO +238 -0
- pulsar_lite-0.1.0/README.md +212 -0
- pulsar_lite-0.1.0/pyproject.toml +53 -0
- pulsar_lite-0.1.0/setup.cfg +4 -0
- pulsar_lite-0.1.0/setup.py +66 -0
- pulsar_lite-0.1.0/src/pulsar_lite/__init__.py +31 -0
- pulsar_lite-0.1.0/src/pulsar_lite/bin/pulsar-lite +0 -0
- pulsar_lite-0.1.0/src/pulsar_lite/binary_finder.py +69 -0
- pulsar_lite-0.1.0/src/pulsar_lite/client.py +116 -0
- pulsar_lite-0.1.0/src/pulsar_lite/process_manager.py +236 -0
- pulsar_lite-0.1.0/src/pulsar_lite.egg-info/PKG-INFO +238 -0
- pulsar_lite-0.1.0/src/pulsar_lite.egg-info/SOURCES.txt +13 -0
- pulsar_lite-0.1.0/src/pulsar_lite.egg-info/dependency_links.txt +1 -0
- pulsar_lite-0.1.0/src/pulsar_lite.egg-info/requires.txt +6 -0
- pulsar_lite-0.1.0/src/pulsar_lite.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pulsar-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Embedded Apache Pulsar-compatible broker for local development
|
|
5
|
+
Author-email: Ascentstream <community@ascentstream.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/ascentstream/pulsar-lite
|
|
8
|
+
Project-URL: Documentation, https://github.com/ascentstream/pulsar-lite#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/ascentstream/pulsar-lite
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: pulsar-client>=3.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Pulsar Lite
|
|
28
|
+
|
|
29
|
+
[](https://github.com/ascentstream/pulsar-lite/blob/main/LICENSE)
|
|
30
|
+
|
|
31
|
+
Pulsar Lite is a lightweight local broker that implements the core Apache Pulsar
|
|
32
|
+
binary protocol. This package ships a small Python helper that can start and
|
|
33
|
+
manage a local broker process, so you can use Pulsar topic names and the
|
|
34
|
+
official Pulsar Python client with a single local file path.
|
|
35
|
+
|
|
36
|
+
It is designed for fast local feedback, not as a production replacement for an
|
|
37
|
+
Apache Pulsar cluster. Use Apache Pulsar for production workloads that require
|
|
38
|
+
multi-broker scheduling, replication, capacity management, tenant isolation, or
|
|
39
|
+
operational SLAs.
|
|
40
|
+
|
|
41
|
+
## Why use the Python helper
|
|
42
|
+
|
|
43
|
+
Many applications only need a local broker to validate the messaging path:
|
|
44
|
+
producers, consumers, subscriptions, flow control, failover, and key-based
|
|
45
|
+
routing. Setting up a full Pulsar deployment can be more expensive than the
|
|
46
|
+
test or prototype itself.
|
|
47
|
+
|
|
48
|
+
`pulsar-lite` keeps the client-facing API close to Pulsar while removing the
|
|
49
|
+
local setup cost:
|
|
50
|
+
|
|
51
|
+
- Start a local broker by passing a file path, no manual process management.
|
|
52
|
+
- Connect with the official `pulsar-client` (installed automatically).
|
|
53
|
+
- Use Pulsar topic names such as `persistent://...` and `non-persistent://...`.
|
|
54
|
+
- Exercise Shared, Failover, Exclusive, and KeyShared subscription behavior.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install pulsar-lite
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The package declares `pulsar-client>=3.0.0` as a runtime dependency, so the
|
|
63
|
+
official Pulsar Python client is installed automatically.
|
|
64
|
+
|
|
65
|
+
Platform wheels include a prebuilt broker binary. If no wheel matches your
|
|
66
|
+
platform, see the [build from source](#building-the-broker-from-source) section.
|
|
67
|
+
|
|
68
|
+
## Quick start: embedded mode
|
|
69
|
+
|
|
70
|
+
Pass a local file path and the helper starts a broker process for you:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import pulsar
|
|
74
|
+
from pulsar_lite import PulsarClient
|
|
75
|
+
|
|
76
|
+
topic = "non-persistent://public/default/quick-start"
|
|
77
|
+
|
|
78
|
+
with PulsarClient("./demo.db") as client:
|
|
79
|
+
consumer = client.subscribe(
|
|
80
|
+
topic,
|
|
81
|
+
"quick-start-sub",
|
|
82
|
+
consumer_type=pulsar.ConsumerType.Shared,
|
|
83
|
+
)
|
|
84
|
+
producer = client.create_producer(topic)
|
|
85
|
+
|
|
86
|
+
producer.send(b"hello from pulsar lite")
|
|
87
|
+
message = consumer.receive(timeout_millis=5000)
|
|
88
|
+
print(message.data().decode("utf-8"))
|
|
89
|
+
consumer.acknowledge(message)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When the `PulsarClient` context exits, the embedded broker process is stopped
|
|
93
|
+
automatically (reference counted, so multiple clients on the same path share
|
|
94
|
+
one broker).
|
|
95
|
+
|
|
96
|
+
## Quick start: remote mode
|
|
97
|
+
|
|
98
|
+
If you already have a broker running (locally or remotely), pass a `pulsar://`
|
|
99
|
+
URI instead and the helper acts as a thin wrapper over `pulsar.Client`:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import pulsar
|
|
103
|
+
from pulsar_lite import PulsarClient
|
|
104
|
+
|
|
105
|
+
with PulsarClient("pulsar://localhost:6650") as client:
|
|
106
|
+
producer = client.create_producer("non-persistent://public/default/events")
|
|
107
|
+
producer.send(b"event-1")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You can also skip the helper entirely and use the official client directly,
|
|
111
|
+
since the broker speaks the standard Pulsar binary protocol:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import pulsar
|
|
115
|
+
|
|
116
|
+
client = pulsar.Client("pulsar://localhost:6650")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Explicit broker lifecycle
|
|
120
|
+
|
|
121
|
+
For cases where you want to start the broker and then connect with the official
|
|
122
|
+
client (or multiple clients), use `start_broker`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import pulsar
|
|
126
|
+
from pulsar_lite import start_broker
|
|
127
|
+
|
|
128
|
+
with start_broker("./demo.db") as broker:
|
|
129
|
+
client = pulsar.Client(broker.url)
|
|
130
|
+
# ... use the official client against broker.url ...
|
|
131
|
+
client.close()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## API
|
|
135
|
+
|
|
136
|
+
### `PulsarClient(uri, **kwargs)`
|
|
137
|
+
|
|
138
|
+
- `uri`:
|
|
139
|
+
- A local file path (e.g. `"./demo.db"`) starts an embedded broker.
|
|
140
|
+
- A `pulsar://` or `pulsar+ssl://` URI connects to an existing broker.
|
|
141
|
+
- `**kwargs` are forwarded to `pulsar.Client`.
|
|
142
|
+
- Any attribute access not defined on `PulsarClient` itself is forwarded to the
|
|
143
|
+
underlying `pulsar.Client`, so the standard Pulsar API (`create_producer`,
|
|
144
|
+
`subscribe`, `get_topic_partitions`, ...) is fully available.
|
|
145
|
+
- Supports `with` statements and auto-closes the embedded broker on exit.
|
|
146
|
+
|
|
147
|
+
Properties:
|
|
148
|
+
|
|
149
|
+
| Property | Description |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `is_embedded` | `True` when the helper started a local broker. |
|
|
152
|
+
| `db_path` | The absolute local file path (embedded mode only). |
|
|
153
|
+
| `pulsar_url` | The `pulsar://localhost:<port>` URL the client connects to. |
|
|
154
|
+
|
|
155
|
+
### `start_broker(db_path) -> BrokerHandle`
|
|
156
|
+
|
|
157
|
+
Starts (or reuses) an embedded broker for the given path and returns a
|
|
158
|
+
`BrokerHandle` with `.url`, `.port`, and a `.stop()` method. Use it as a
|
|
159
|
+
context manager for automatic cleanup.
|
|
160
|
+
|
|
161
|
+
## Topic names and subscription modes
|
|
162
|
+
|
|
163
|
+
Pulsar Lite accepts standard Pulsar topic URIs:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
persistent://public/default/my-topic
|
|
167
|
+
non-persistent://public/default/my-topic
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use `non-persistent://...` for live event dispatch where slow or disconnected
|
|
171
|
+
consumers should not create a durable backlog. Use `persistent://...` when a
|
|
172
|
+
test requires stored entries, cursor replay, or acknowledgements across restart
|
|
173
|
+
(requires a broker binary built with RocksDB storage support).
|
|
174
|
+
|
|
175
|
+
Supported subscription modes:
|
|
176
|
+
|
|
177
|
+
| Mode | Summary |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| Exclusive | One active consumer; additional consumers are rejected. |
|
|
180
|
+
| Failover | One active consumer with standby takeover. |
|
|
181
|
+
| Shared | Messages are distributed across available consumers. |
|
|
182
|
+
| KeyShared | Messages with the same key are routed to the same consumer. |
|
|
183
|
+
|
|
184
|
+
## Binary discovery
|
|
185
|
+
|
|
186
|
+
The helper looks for the bundled broker binary in this order:
|
|
187
|
+
|
|
188
|
+
1. The `pulsar_lite/bin/` directory shipped inside the wheel.
|
|
189
|
+
2. The `PULSAR_LITE_BINARY` environment variable.
|
|
190
|
+
3. The Rust release output at `rust/target/release/pulsar-lite` (development mode).
|
|
191
|
+
4. The system `PATH`.
|
|
192
|
+
5. Common install locations such as `/usr/local/bin/pulsar-lite`.
|
|
193
|
+
|
|
194
|
+
If you build the broker yourself, point `PULSAR_LITE_BINARY` at the resulting
|
|
195
|
+
binary instead of reinstalling the package.
|
|
196
|
+
|
|
197
|
+
## Building the broker from source
|
|
198
|
+
|
|
199
|
+
If no prebuilt wheel matches your platform, build the broker from source and
|
|
200
|
+
let the helper discover it via `PULSAR_LITE_BINARY`:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
git clone https://github.com/ascentstream/pulsar-lite.git
|
|
204
|
+
cd pulsar-lite/rust
|
|
205
|
+
cargo build --release # core build
|
|
206
|
+
cargo build --release --features rocksdb-storage # with persistent storage
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Then install the Python package and point it at your binary:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
cd ../python
|
|
213
|
+
pip install -e .
|
|
214
|
+
export PULSAR_LITE_BINARY=$(pwd)/../rust/target/release/pulsar-lite
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Links
|
|
218
|
+
|
|
219
|
+
- Source: <https://github.com/ascentstream/pulsar-lite>
|
|
220
|
+
- Issues: <https://github.com/ascentstream/pulsar-lite/issues>
|
|
221
|
+
- Full documentation: <https://github.com/ascentstream/pulsar-lite#readme>
|
|
222
|
+
|
|
223
|
+
## Project boundaries
|
|
224
|
+
|
|
225
|
+
Pulsar Lite intentionally does not provide:
|
|
226
|
+
|
|
227
|
+
- Multi-broker coordination or load balancing.
|
|
228
|
+
- Cross-cluster replication.
|
|
229
|
+
- Production-grade authorization or tenant governance.
|
|
230
|
+
- BookKeeper compatibility.
|
|
231
|
+
- Production durability or availability guarantees.
|
|
232
|
+
|
|
233
|
+
The project is useful for local development and compatibility testing, but
|
|
234
|
+
production deployments should use Apache Pulsar.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
Pulsar Lite is licensed under the [Apache License 2.0](https://github.com/ascentstream/pulsar-lite/blob/main/LICENSE).
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# Pulsar Lite
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ascentstream/pulsar-lite/blob/main/LICENSE)
|
|
4
|
+
|
|
5
|
+
Pulsar Lite is a lightweight local broker that implements the core Apache Pulsar
|
|
6
|
+
binary protocol. This package ships a small Python helper that can start and
|
|
7
|
+
manage a local broker process, so you can use Pulsar topic names and the
|
|
8
|
+
official Pulsar Python client with a single local file path.
|
|
9
|
+
|
|
10
|
+
It is designed for fast local feedback, not as a production replacement for an
|
|
11
|
+
Apache Pulsar cluster. Use Apache Pulsar for production workloads that require
|
|
12
|
+
multi-broker scheduling, replication, capacity management, tenant isolation, or
|
|
13
|
+
operational SLAs.
|
|
14
|
+
|
|
15
|
+
## Why use the Python helper
|
|
16
|
+
|
|
17
|
+
Many applications only need a local broker to validate the messaging path:
|
|
18
|
+
producers, consumers, subscriptions, flow control, failover, and key-based
|
|
19
|
+
routing. Setting up a full Pulsar deployment can be more expensive than the
|
|
20
|
+
test or prototype itself.
|
|
21
|
+
|
|
22
|
+
`pulsar-lite` keeps the client-facing API close to Pulsar while removing the
|
|
23
|
+
local setup cost:
|
|
24
|
+
|
|
25
|
+
- Start a local broker by passing a file path, no manual process management.
|
|
26
|
+
- Connect with the official `pulsar-client` (installed automatically).
|
|
27
|
+
- Use Pulsar topic names such as `persistent://...` and `non-persistent://...`.
|
|
28
|
+
- Exercise Shared, Failover, Exclusive, and KeyShared subscription behavior.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install pulsar-lite
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The package declares `pulsar-client>=3.0.0` as a runtime dependency, so the
|
|
37
|
+
official Pulsar Python client is installed automatically.
|
|
38
|
+
|
|
39
|
+
Platform wheels include a prebuilt broker binary. If no wheel matches your
|
|
40
|
+
platform, see the [build from source](#building-the-broker-from-source) section.
|
|
41
|
+
|
|
42
|
+
## Quick start: embedded mode
|
|
43
|
+
|
|
44
|
+
Pass a local file path and the helper starts a broker process for you:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import pulsar
|
|
48
|
+
from pulsar_lite import PulsarClient
|
|
49
|
+
|
|
50
|
+
topic = "non-persistent://public/default/quick-start"
|
|
51
|
+
|
|
52
|
+
with PulsarClient("./demo.db") as client:
|
|
53
|
+
consumer = client.subscribe(
|
|
54
|
+
topic,
|
|
55
|
+
"quick-start-sub",
|
|
56
|
+
consumer_type=pulsar.ConsumerType.Shared,
|
|
57
|
+
)
|
|
58
|
+
producer = client.create_producer(topic)
|
|
59
|
+
|
|
60
|
+
producer.send(b"hello from pulsar lite")
|
|
61
|
+
message = consumer.receive(timeout_millis=5000)
|
|
62
|
+
print(message.data().decode("utf-8"))
|
|
63
|
+
consumer.acknowledge(message)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
When the `PulsarClient` context exits, the embedded broker process is stopped
|
|
67
|
+
automatically (reference counted, so multiple clients on the same path share
|
|
68
|
+
one broker).
|
|
69
|
+
|
|
70
|
+
## Quick start: remote mode
|
|
71
|
+
|
|
72
|
+
If you already have a broker running (locally or remotely), pass a `pulsar://`
|
|
73
|
+
URI instead and the helper acts as a thin wrapper over `pulsar.Client`:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import pulsar
|
|
77
|
+
from pulsar_lite import PulsarClient
|
|
78
|
+
|
|
79
|
+
with PulsarClient("pulsar://localhost:6650") as client:
|
|
80
|
+
producer = client.create_producer("non-persistent://public/default/events")
|
|
81
|
+
producer.send(b"event-1")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You can also skip the helper entirely and use the official client directly,
|
|
85
|
+
since the broker speaks the standard Pulsar binary protocol:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import pulsar
|
|
89
|
+
|
|
90
|
+
client = pulsar.Client("pulsar://localhost:6650")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Explicit broker lifecycle
|
|
94
|
+
|
|
95
|
+
For cases where you want to start the broker and then connect with the official
|
|
96
|
+
client (or multiple clients), use `start_broker`:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import pulsar
|
|
100
|
+
from pulsar_lite import start_broker
|
|
101
|
+
|
|
102
|
+
with start_broker("./demo.db") as broker:
|
|
103
|
+
client = pulsar.Client(broker.url)
|
|
104
|
+
# ... use the official client against broker.url ...
|
|
105
|
+
client.close()
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## API
|
|
109
|
+
|
|
110
|
+
### `PulsarClient(uri, **kwargs)`
|
|
111
|
+
|
|
112
|
+
- `uri`:
|
|
113
|
+
- A local file path (e.g. `"./demo.db"`) starts an embedded broker.
|
|
114
|
+
- A `pulsar://` or `pulsar+ssl://` URI connects to an existing broker.
|
|
115
|
+
- `**kwargs` are forwarded to `pulsar.Client`.
|
|
116
|
+
- Any attribute access not defined on `PulsarClient` itself is forwarded to the
|
|
117
|
+
underlying `pulsar.Client`, so the standard Pulsar API (`create_producer`,
|
|
118
|
+
`subscribe`, `get_topic_partitions`, ...) is fully available.
|
|
119
|
+
- Supports `with` statements and auto-closes the embedded broker on exit.
|
|
120
|
+
|
|
121
|
+
Properties:
|
|
122
|
+
|
|
123
|
+
| Property | Description |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `is_embedded` | `True` when the helper started a local broker. |
|
|
126
|
+
| `db_path` | The absolute local file path (embedded mode only). |
|
|
127
|
+
| `pulsar_url` | The `pulsar://localhost:<port>` URL the client connects to. |
|
|
128
|
+
|
|
129
|
+
### `start_broker(db_path) -> BrokerHandle`
|
|
130
|
+
|
|
131
|
+
Starts (or reuses) an embedded broker for the given path and returns a
|
|
132
|
+
`BrokerHandle` with `.url`, `.port`, and a `.stop()` method. Use it as a
|
|
133
|
+
context manager for automatic cleanup.
|
|
134
|
+
|
|
135
|
+
## Topic names and subscription modes
|
|
136
|
+
|
|
137
|
+
Pulsar Lite accepts standard Pulsar topic URIs:
|
|
138
|
+
|
|
139
|
+
```text
|
|
140
|
+
persistent://public/default/my-topic
|
|
141
|
+
non-persistent://public/default/my-topic
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use `non-persistent://...` for live event dispatch where slow or disconnected
|
|
145
|
+
consumers should not create a durable backlog. Use `persistent://...` when a
|
|
146
|
+
test requires stored entries, cursor replay, or acknowledgements across restart
|
|
147
|
+
(requires a broker binary built with RocksDB storage support).
|
|
148
|
+
|
|
149
|
+
Supported subscription modes:
|
|
150
|
+
|
|
151
|
+
| Mode | Summary |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| Exclusive | One active consumer; additional consumers are rejected. |
|
|
154
|
+
| Failover | One active consumer with standby takeover. |
|
|
155
|
+
| Shared | Messages are distributed across available consumers. |
|
|
156
|
+
| KeyShared | Messages with the same key are routed to the same consumer. |
|
|
157
|
+
|
|
158
|
+
## Binary discovery
|
|
159
|
+
|
|
160
|
+
The helper looks for the bundled broker binary in this order:
|
|
161
|
+
|
|
162
|
+
1. The `pulsar_lite/bin/` directory shipped inside the wheel.
|
|
163
|
+
2. The `PULSAR_LITE_BINARY` environment variable.
|
|
164
|
+
3. The Rust release output at `rust/target/release/pulsar-lite` (development mode).
|
|
165
|
+
4. The system `PATH`.
|
|
166
|
+
5. Common install locations such as `/usr/local/bin/pulsar-lite`.
|
|
167
|
+
|
|
168
|
+
If you build the broker yourself, point `PULSAR_LITE_BINARY` at the resulting
|
|
169
|
+
binary instead of reinstalling the package.
|
|
170
|
+
|
|
171
|
+
## Building the broker from source
|
|
172
|
+
|
|
173
|
+
If no prebuilt wheel matches your platform, build the broker from source and
|
|
174
|
+
let the helper discover it via `PULSAR_LITE_BINARY`:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/ascentstream/pulsar-lite.git
|
|
178
|
+
cd pulsar-lite/rust
|
|
179
|
+
cargo build --release # core build
|
|
180
|
+
cargo build --release --features rocksdb-storage # with persistent storage
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Then install the Python package and point it at your binary:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cd ../python
|
|
187
|
+
pip install -e .
|
|
188
|
+
export PULSAR_LITE_BINARY=$(pwd)/../rust/target/release/pulsar-lite
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Links
|
|
192
|
+
|
|
193
|
+
- Source: <https://github.com/ascentstream/pulsar-lite>
|
|
194
|
+
- Issues: <https://github.com/ascentstream/pulsar-lite/issues>
|
|
195
|
+
- Full documentation: <https://github.com/ascentstream/pulsar-lite#readme>
|
|
196
|
+
|
|
197
|
+
## Project boundaries
|
|
198
|
+
|
|
199
|
+
Pulsar Lite intentionally does not provide:
|
|
200
|
+
|
|
201
|
+
- Multi-broker coordination or load balancing.
|
|
202
|
+
- Cross-cluster replication.
|
|
203
|
+
- Production-grade authorization or tenant governance.
|
|
204
|
+
- BookKeeper compatibility.
|
|
205
|
+
- Production durability or availability guarantees.
|
|
206
|
+
|
|
207
|
+
The project is useful for local development and compatibility testing, but
|
|
208
|
+
production deployments should use Apache Pulsar.
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
Pulsar Lite is licensed under the [Apache License 2.0](https://github.com/ascentstream/pulsar-lite/blob/main/LICENSE).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pulsar-lite"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Embedded Apache Pulsar-compatible broker for local development"
|
|
9
|
+
authors = [{name = "Ascentstream", email = "community@ascentstream.com"}]
|
|
10
|
+
license = {text = "Apache-2.0"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.8",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pulsar-client>=3.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=7.0",
|
|
31
|
+
"black>=23.0",
|
|
32
|
+
"ruff>=0.1.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/ascentstream/pulsar-lite"
|
|
37
|
+
Documentation = "https://github.com/ascentstream/pulsar-lite#readme"
|
|
38
|
+
Repository = "https://github.com/ascentstream/pulsar-lite"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
pulsar_lite = ["bin/pulsar-lite", "bin/pulsar-lite.exe"]
|
|
45
|
+
|
|
46
|
+
[tool.black]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = ['py38', 'py39', 'py310', 'py311', 'py312']
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 100
|
|
52
|
+
target-version = "py38"
|
|
53
|
+
select = ["E", "F", "I", "N", "W"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pulsar Lite setup customizations."""
|
|
3
|
+
|
|
4
|
+
import platform
|
|
5
|
+
import sys
|
|
6
|
+
import sysconfig
|
|
7
|
+
|
|
8
|
+
from setuptools import setup
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from setuptools.command.bdist_wheel import bdist_wheel
|
|
12
|
+
except ImportError:
|
|
13
|
+
from wheel.bdist_wheel import bdist_wheel
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class bdist_platform_wheel(bdist_wheel):
|
|
17
|
+
"""Build py3-none-platform wheels for the bundled Rust broker binary."""
|
|
18
|
+
|
|
19
|
+
def finalize_options(self):
|
|
20
|
+
super().finalize_options()
|
|
21
|
+
self.root_is_pure = False
|
|
22
|
+
|
|
23
|
+
def get_tag(self):
|
|
24
|
+
if self.plat_name_supplied:
|
|
25
|
+
plat_name = self.plat_name
|
|
26
|
+
elif self.plat_name and not self.plat_name.startswith("macosx"):
|
|
27
|
+
plat_name = self.plat_name
|
|
28
|
+
else:
|
|
29
|
+
plat_name = sysconfig.get_platform()
|
|
30
|
+
plat_name = _native_macos_platform(plat_name)
|
|
31
|
+
|
|
32
|
+
# Normalize to PEP 425 form (lowercase, underscores, no dots).
|
|
33
|
+
plat_name = plat_name.lower().replace("-", "_").replace(".", "_")
|
|
34
|
+
|
|
35
|
+
# PyPI rejects bare "linux_*" platform tags (PEP 513/599/600). The
|
|
36
|
+
# bundled Rust broker is a static binary with no glibc/ELIB coupling,
|
|
37
|
+
# so we relabel Linux wheels to manylinux_2_17 which is accepted by
|
|
38
|
+
# PyPI and works on any glibc >= 2.17 (CentOS 7+) distribution.
|
|
39
|
+
if plat_name == "linux_x86_64":
|
|
40
|
+
plat_name = "manylinux_2_17_x86_64"
|
|
41
|
+
elif plat_name == "linux_i686":
|
|
42
|
+
plat_name = "manylinux_2_17_i686"
|
|
43
|
+
elif plat_name == "linux_aarch64":
|
|
44
|
+
plat_name = "manylinux_2_17_aarch64"
|
|
45
|
+
|
|
46
|
+
# 32-bit x86 fallback for legacy i686 detection.
|
|
47
|
+
if plat_name in ("linux-x86_64", "linux_x86_64") and sys.maxsize == 2147483647:
|
|
48
|
+
plat_name = "manylinux_2_17_i686"
|
|
49
|
+
|
|
50
|
+
return self.python_tag, "none", plat_name
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _native_macos_platform(plat_name):
|
|
54
|
+
if not plat_name.startswith("macosx") or "universal2" not in plat_name:
|
|
55
|
+
return plat_name
|
|
56
|
+
|
|
57
|
+
machine = platform.machine().lower()
|
|
58
|
+
if machine in ("arm64", "aarch64"):
|
|
59
|
+
return "macosx-11.0-arm64"
|
|
60
|
+
if machine in ("x86_64", "amd64"):
|
|
61
|
+
return "macosx-10.9-x86_64"
|
|
62
|
+
return plat_name
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
setup(cmdclass={"bdist_wheel": bdist_platform_wheel})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pulsar Lite - 嵌入式轻量级消息队列
|
|
3
|
+
|
|
4
|
+
使用方式类似 Milvus Lite:
|
|
5
|
+
from pulsar_lite import PulsarClient
|
|
6
|
+
|
|
7
|
+
# 嵌入式模式 - 使用本地文件自动启动
|
|
8
|
+
client = PulsarClient("./milvus_demo.db")
|
|
9
|
+
|
|
10
|
+
# 远程模式 - 连接到远程服务器
|
|
11
|
+
client = PulsarClient("pulsar://localhost:6650")
|
|
12
|
+
|
|
13
|
+
# 使用标准 Pulsar API
|
|
14
|
+
producer = client.create_producer("my-topic")
|
|
15
|
+
producer.send(b"Hello World!")
|
|
16
|
+
client.close()
|
|
17
|
+
|
|
18
|
+
# 显式启动 broker,然后使用官方 pulsar-client
|
|
19
|
+
from pulsar_lite import start_broker
|
|
20
|
+
import pulsar
|
|
21
|
+
|
|
22
|
+
broker = start_broker("./milvus_demo.db")
|
|
23
|
+
client = pulsar.Client(broker.url)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
__version__ = "0.1.0"
|
|
27
|
+
|
|
28
|
+
from .client import PulsarClient
|
|
29
|
+
from .process_manager import BrokerHandle, start_broker
|
|
30
|
+
|
|
31
|
+
__all__ = ["BrokerHandle", "PulsarClient", "start_broker", "__version__"]
|
|
Binary file
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
查找 Pulsar Lite 二进制文件
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def find_pulsar_lite_binary() -> str:
|
|
13
|
+
"""
|
|
14
|
+
查找 pulsar-lite 可执行文件
|
|
15
|
+
|
|
16
|
+
搜索顺序:
|
|
17
|
+
1. Python 包内置二进制(pip wheel 安装模式)
|
|
18
|
+
2. 环境变量 PULSAR_LITE_BINARY
|
|
19
|
+
3. 当前 Python 包目录的相对路径(开发模式)
|
|
20
|
+
4. 系统 PATH
|
|
21
|
+
5. 常见安装位置
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
二进制文件的绝对路径
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
FileNotFoundError: 找不到二进制文件
|
|
28
|
+
"""
|
|
29
|
+
# 1. 检查 Python 包内置二进制
|
|
30
|
+
package_binary_name = "pulsar-lite.exe" if sys.platform.startswith("win") else "pulsar-lite"
|
|
31
|
+
packaged_binary = Path(__file__).parent / "bin" / package_binary_name
|
|
32
|
+
if packaged_binary.is_file():
|
|
33
|
+
return str(packaged_binary.absolute())
|
|
34
|
+
|
|
35
|
+
# 2. 检查环境变量
|
|
36
|
+
env_path = os.environ.get("PULSAR_LITE_BINARY")
|
|
37
|
+
if env_path and os.path.isfile(env_path):
|
|
38
|
+
return os.path.abspath(env_path)
|
|
39
|
+
|
|
40
|
+
# 3. 开发模式:相对于当前包的路径
|
|
41
|
+
package_dir = Path(__file__).parent.parent.parent.parent # python/src/pulsar_lite -> 项目根目录
|
|
42
|
+
dev_binary = package_dir / "rust" / "target" / "release" / "pulsar-lite"
|
|
43
|
+
if dev_binary.exists():
|
|
44
|
+
return str(dev_binary.absolute())
|
|
45
|
+
|
|
46
|
+
# 4. 在 PATH 中查找
|
|
47
|
+
binary_name = package_binary_name
|
|
48
|
+
in_path = shutil.which(binary_name)
|
|
49
|
+
if in_path:
|
|
50
|
+
return in_path
|
|
51
|
+
|
|
52
|
+
# 5. 常见安装位置
|
|
53
|
+
common_paths = [
|
|
54
|
+
"/usr/local/bin/pulsar-lite",
|
|
55
|
+
"/usr/bin/pulsar-lite",
|
|
56
|
+
Path.home() / ".local" / "bin" / "pulsar-lite",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
for path in common_paths:
|
|
60
|
+
path = Path(path)
|
|
61
|
+
if path.exists():
|
|
62
|
+
return str(path.absolute())
|
|
63
|
+
|
|
64
|
+
raise FileNotFoundError(
|
|
65
|
+
"Pulsar Lite binary not found. Please either:\n"
|
|
66
|
+
"1. Set PULSAR_LITE_BINARY environment variable\n"
|
|
67
|
+
"2. Build from source: cd rust && cargo build --release\n"
|
|
68
|
+
"3. Install pulsar-lite to your PATH"
|
|
69
|
+
)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pulsar Lite 客户端
|
|
3
|
+
|
|
4
|
+
提供与 Milvus Lite 类似的无缝体验:
|
|
5
|
+
- 本地文件路径 → 自动启动嵌入式服务器
|
|
6
|
+
- 远程 URI → 直接连接远程服务器
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import pulsar
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, Any
|
|
12
|
+
from .process_manager import process_manager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class PulsarClient:
|
|
16
|
+
"""
|
|
17
|
+
Pulsar Lite 客户端(兼容 Pulsar Python SDK API)
|
|
18
|
+
|
|
19
|
+
使用方式类似 Milvus Lite:
|
|
20
|
+
# 嵌入式模式 - 使用本地文件自动启动
|
|
21
|
+
client = PulsarClient("./demo.db")
|
|
22
|
+
|
|
23
|
+
# 远程模式 - 连接到远程服务器
|
|
24
|
+
client = PulsarClient("pulsar://localhost:6650")
|
|
25
|
+
|
|
26
|
+
# 后续使用完全兼容 pulsar.Client API
|
|
27
|
+
producer = client.create_producer("my-topic")
|
|
28
|
+
producer.send(b"Hello")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, uri: str, **kwargs):
|
|
32
|
+
"""
|
|
33
|
+
初始化客户端
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
uri: 连接地址,可以是:
|
|
37
|
+
- 本地文件路径(如 "./demo.db")→ 自动启动嵌入式服务器
|
|
38
|
+
- Pulsar URI(如 "pulsar://localhost:6650")→ 直接连接远程
|
|
39
|
+
**kwargs: 传递给 pulsar.Client 的其他参数
|
|
40
|
+
"""
|
|
41
|
+
self._original_uri = uri
|
|
42
|
+
self._db_path: Optional[str] = None
|
|
43
|
+
self._is_embedded = False
|
|
44
|
+
self._client: Optional[pulsar.Client] = None
|
|
45
|
+
|
|
46
|
+
# 判断是本地文件还是远程 URI
|
|
47
|
+
is_remote = uri.startswith("pulsar://") or uri.startswith("pulsar+ssl://")
|
|
48
|
+
|
|
49
|
+
if is_remote:
|
|
50
|
+
# 远程模式:直接连接
|
|
51
|
+
self._pulsar_url = uri
|
|
52
|
+
else:
|
|
53
|
+
# 嵌入式模式:启动本地服务器
|
|
54
|
+
self._db_path = str(Path(uri).absolute())
|
|
55
|
+
self._pulsar_url, self._port = process_manager.start_server(self._db_path)
|
|
56
|
+
self._is_embedded = True
|
|
57
|
+
|
|
58
|
+
# 创建实际的 Pulsar 客户端
|
|
59
|
+
self._client = pulsar.Client(self._pulsar_url, **kwargs)
|
|
60
|
+
|
|
61
|
+
def __getattr__(self, name: str) -> Any:
|
|
62
|
+
"""
|
|
63
|
+
代理所有其他方法到 pulsar.Client
|
|
64
|
+
|
|
65
|
+
这样用户可以使用所有标准的 Pulsar API,如:
|
|
66
|
+
- create_producer
|
|
67
|
+
- subscribe
|
|
68
|
+
- get_topic_partitions
|
|
69
|
+
- 等等...
|
|
70
|
+
"""
|
|
71
|
+
if self._client is None:
|
|
72
|
+
raise RuntimeError("Client has been closed")
|
|
73
|
+
|
|
74
|
+
return getattr(self._client, name)
|
|
75
|
+
|
|
76
|
+
def close(self):
|
|
77
|
+
"""关闭客户端并释放资源"""
|
|
78
|
+
if self._client is not None:
|
|
79
|
+
self._client.close()
|
|
80
|
+
self._client = None
|
|
81
|
+
|
|
82
|
+
# 如果是嵌入式模式,停止服务器
|
|
83
|
+
if self._is_embedded and self._db_path:
|
|
84
|
+
process_manager.stop_server(self._db_path)
|
|
85
|
+
self._is_embedded = False
|
|
86
|
+
|
|
87
|
+
def __enter__(self):
|
|
88
|
+
"""支持 with 语句"""
|
|
89
|
+
return self
|
|
90
|
+
|
|
91
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
92
|
+
"""退出 with 语句时自动关闭"""
|
|
93
|
+
self.close()
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
def __del__(self):
|
|
97
|
+
"""析构时自动关闭"""
|
|
98
|
+
try:
|
|
99
|
+
self.close()
|
|
100
|
+
except:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def is_embedded(self) -> bool:
|
|
105
|
+
"""是否为嵌入式模式"""
|
|
106
|
+
return self._is_embedded
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def db_path(self) -> Optional[str]:
|
|
110
|
+
"""数据库文件路径(仅嵌入式模式有效)"""
|
|
111
|
+
return self._db_path
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def pulsar_url(self) -> str:
|
|
115
|
+
"""Pulsar 连接 URL"""
|
|
116
|
+
return self._pulsar_url
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pulsar Lite 进程管理器
|
|
3
|
+
|
|
4
|
+
参考 Milvus Lite 的实现,管理嵌入式服务器进程
|
|
5
|
+
支持多个客户端共享同一服务器实例
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import time
|
|
11
|
+
import threading
|
|
12
|
+
import socket
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Dict, Tuple, Optional
|
|
16
|
+
from .binary_finder import find_pulsar_lite_binary
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class BrokerHandle:
|
|
21
|
+
"""Handle for an embedded Pulsar Lite broker process."""
|
|
22
|
+
|
|
23
|
+
db_path: str
|
|
24
|
+
url: str
|
|
25
|
+
port: int
|
|
26
|
+
|
|
27
|
+
def stop(self):
|
|
28
|
+
"""Release this handle and stop the broker when no clients remain."""
|
|
29
|
+
process_manager.stop_server(self.db_path)
|
|
30
|
+
|
|
31
|
+
def __enter__(self):
|
|
32
|
+
return self
|
|
33
|
+
|
|
34
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
35
|
+
self.stop()
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ProcessManager:
|
|
40
|
+
"""
|
|
41
|
+
单例进程管理器
|
|
42
|
+
|
|
43
|
+
职责:
|
|
44
|
+
1. 启动/停止 Pulsar Lite 服务器进程
|
|
45
|
+
2. 引用计数:支持多个客户端共享同一实例
|
|
46
|
+
3. 自动端口分配
|
|
47
|
+
4. 线程安全
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
_instance = None
|
|
51
|
+
_lock = threading.Lock()
|
|
52
|
+
|
|
53
|
+
def __new__(cls):
|
|
54
|
+
if cls._instance is None:
|
|
55
|
+
with cls._lock:
|
|
56
|
+
if cls._instance is None:
|
|
57
|
+
cls._instance = super().__new__(cls)
|
|
58
|
+
cls._instance._initialized = False
|
|
59
|
+
return cls._instance
|
|
60
|
+
|
|
61
|
+
def __init__(self):
|
|
62
|
+
if self._initialized:
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
self._initialized = True
|
|
66
|
+
self._processes: Dict[str, Tuple[subprocess.Popen, int, int]] = {} # db_path -> (process, ref_count, port)
|
|
67
|
+
self._process_lock = threading.Lock()
|
|
68
|
+
self._binary_path = None
|
|
69
|
+
|
|
70
|
+
def _find_free_port(self, start_port: int = 6650) -> int:
|
|
71
|
+
"""查找可用端口"""
|
|
72
|
+
port = start_port
|
|
73
|
+
while port < 6700:
|
|
74
|
+
try:
|
|
75
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
76
|
+
s.bind(("127.0.0.1", port))
|
|
77
|
+
return port
|
|
78
|
+
except OSError:
|
|
79
|
+
port += 1
|
|
80
|
+
raise RuntimeError("No available port found")
|
|
81
|
+
|
|
82
|
+
def _wait_until_ready(self, process: subprocess.Popen, port: int, timeout: float = 10.0):
|
|
83
|
+
"""Wait until the broker accepts TCP connections on the selected port."""
|
|
84
|
+
deadline = time.time() + timeout
|
|
85
|
+
last_error = None
|
|
86
|
+
|
|
87
|
+
while time.time() < deadline:
|
|
88
|
+
if process.poll() is not None:
|
|
89
|
+
raise RuntimeError("Pulsar Lite server exited before accepting connections")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
with socket.create_connection(("127.0.0.1", port), timeout=0.25):
|
|
93
|
+
return
|
|
94
|
+
except OSError as error:
|
|
95
|
+
last_error = error
|
|
96
|
+
time.sleep(0.1)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
process.terminate()
|
|
100
|
+
process.wait(timeout=5)
|
|
101
|
+
except Exception:
|
|
102
|
+
process.kill()
|
|
103
|
+
process.wait()
|
|
104
|
+
raise RuntimeError(f"Pulsar Lite server did not become ready on port {port}: {last_error}")
|
|
105
|
+
|
|
106
|
+
def _is_remote_uri(self, uri: str) -> bool:
|
|
107
|
+
"""判断是否为远程 URI"""
|
|
108
|
+
return uri.startswith("pulsar://") or uri.startswith("pulsar+ssl://")
|
|
109
|
+
|
|
110
|
+
def start_server(self, db_path: str) -> Tuple[str, int]:
|
|
111
|
+
"""
|
|
112
|
+
启动嵌入式服务器
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
db_path: 数据库文件路径
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
(pulsar_url, port) - Pulsar 连接 URL 和端口号
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
RuntimeError: 启动失败
|
|
122
|
+
"""
|
|
123
|
+
# 规范化路径
|
|
124
|
+
db_path = str(Path(db_path).absolute())
|
|
125
|
+
|
|
126
|
+
with self._process_lock:
|
|
127
|
+
# 如果已经运行,增加引用计数
|
|
128
|
+
if db_path in self._processes:
|
|
129
|
+
process, ref_count, port = self._processes[db_path]
|
|
130
|
+
self._processes[db_path] = (process, ref_count + 1, port)
|
|
131
|
+
print(f"Reusing Pulsar Lite server for {db_path}, ref_count={ref_count + 1}, port={port}")
|
|
132
|
+
return f"pulsar://localhost:{port}", port
|
|
133
|
+
|
|
134
|
+
# 查找二进制文件
|
|
135
|
+
if self._binary_path is None:
|
|
136
|
+
self._binary_path = find_pulsar_lite_binary()
|
|
137
|
+
|
|
138
|
+
# 查找可用端口
|
|
139
|
+
port = self._find_free_port()
|
|
140
|
+
addr = f"127.0.0.1:{port}"
|
|
141
|
+
|
|
142
|
+
# 确保数据库目录存在
|
|
143
|
+
db_dir = Path(db_path).parent
|
|
144
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
|
|
146
|
+
# 启动进程,日志输出到 /tmp/pulsar-lite.log
|
|
147
|
+
env = {**os.environ, "RUST_LOG": "info"}
|
|
148
|
+
log_file = open("/tmp/pulsar-lite.log", "a")
|
|
149
|
+
process = subprocess.Popen(
|
|
150
|
+
[
|
|
151
|
+
self._binary_path,
|
|
152
|
+
"--addr",
|
|
153
|
+
addr,
|
|
154
|
+
"--db-path",
|
|
155
|
+
db_path,
|
|
156
|
+
],
|
|
157
|
+
env=env,
|
|
158
|
+
stdout=log_file,
|
|
159
|
+
stderr=log_file,
|
|
160
|
+
cwd=str(db_dir)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
self._wait_until_ready(process, port)
|
|
165
|
+
except RuntimeError as error:
|
|
166
|
+
raise RuntimeError(f"Pulsar Lite server failed to start for {db_path}: {error}") from error
|
|
167
|
+
|
|
168
|
+
# 保存进程信息
|
|
169
|
+
self._processes[db_path] = (process, 1, port)
|
|
170
|
+
print(f"Started Pulsar Lite server for {db_path}, ref_count=1, port={port}, pid={process.pid}")
|
|
171
|
+
|
|
172
|
+
return f"pulsar://localhost:{port}", port
|
|
173
|
+
|
|
174
|
+
def start_broker(self, db_path: str) -> BrokerHandle:
|
|
175
|
+
"""Start or reuse an embedded broker and return an explicit lifecycle handle."""
|
|
176
|
+
url, port = self.start_server(db_path)
|
|
177
|
+
return BrokerHandle(db_path=str(Path(db_path).absolute()), url=url, port=port)
|
|
178
|
+
|
|
179
|
+
def stop_server(self, db_path: str):
|
|
180
|
+
"""
|
|
181
|
+
停止嵌入式服务器(减少引用计数,为0时真正停止)
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
db_path: 数据库文件路径
|
|
185
|
+
"""
|
|
186
|
+
db_path = str(Path(db_path).absolute())
|
|
187
|
+
|
|
188
|
+
with self._process_lock:
|
|
189
|
+
if db_path not in self._processes:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
process, ref_count, port = self._processes[db_path]
|
|
193
|
+
ref_count -= 1
|
|
194
|
+
|
|
195
|
+
if ref_count == 0:
|
|
196
|
+
# 引用计数为0,停止服务器
|
|
197
|
+
print(f"Stopping Pulsar Lite server for {db_path}, pid={process.pid}")
|
|
198
|
+
try:
|
|
199
|
+
process.terminate()
|
|
200
|
+
process.wait(timeout=5)
|
|
201
|
+
except:
|
|
202
|
+
process.kill()
|
|
203
|
+
process.wait()
|
|
204
|
+
|
|
205
|
+
del self._processes[db_path]
|
|
206
|
+
print(f"Stopped Pulsar Lite server for {db_path}")
|
|
207
|
+
else:
|
|
208
|
+
# 更新引用计数
|
|
209
|
+
self._processes[db_path] = (process, ref_count, port)
|
|
210
|
+
print(f"Decreased ref_count for {db_path}, ref_count={ref_count}")
|
|
211
|
+
|
|
212
|
+
def stop_all(self):
|
|
213
|
+
"""停止所有服务器"""
|
|
214
|
+
with self._process_lock:
|
|
215
|
+
for db_path, (process, _, _) in list(self._processes.items()):
|
|
216
|
+
print(f"Stopping Pulsar Lite server for {db_path}, pid={process.pid}")
|
|
217
|
+
try:
|
|
218
|
+
process.terminate()
|
|
219
|
+
process.wait(timeout=5)
|
|
220
|
+
except:
|
|
221
|
+
process.kill()
|
|
222
|
+
process.wait()
|
|
223
|
+
|
|
224
|
+
self._processes.clear()
|
|
225
|
+
|
|
226
|
+
def __del__(self):
|
|
227
|
+
self.stop_all()
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# 全局单例
|
|
231
|
+
process_manager = ProcessManager()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def start_broker(db_path: str) -> BrokerHandle:
|
|
235
|
+
"""Start or reuse an embedded Pulsar Lite broker."""
|
|
236
|
+
return process_manager.start_broker(db_path)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pulsar-lite
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Embedded Apache Pulsar-compatible broker for local development
|
|
5
|
+
Author-email: Ascentstream <community@ascentstream.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/ascentstream/pulsar-lite
|
|
8
|
+
Project-URL: Documentation, https://github.com/ascentstream/pulsar-lite#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/ascentstream/pulsar-lite
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Requires-Python: >=3.8
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: pulsar-client>=3.0.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# Pulsar Lite
|
|
28
|
+
|
|
29
|
+
[](https://github.com/ascentstream/pulsar-lite/blob/main/LICENSE)
|
|
30
|
+
|
|
31
|
+
Pulsar Lite is a lightweight local broker that implements the core Apache Pulsar
|
|
32
|
+
binary protocol. This package ships a small Python helper that can start and
|
|
33
|
+
manage a local broker process, so you can use Pulsar topic names and the
|
|
34
|
+
official Pulsar Python client with a single local file path.
|
|
35
|
+
|
|
36
|
+
It is designed for fast local feedback, not as a production replacement for an
|
|
37
|
+
Apache Pulsar cluster. Use Apache Pulsar for production workloads that require
|
|
38
|
+
multi-broker scheduling, replication, capacity management, tenant isolation, or
|
|
39
|
+
operational SLAs.
|
|
40
|
+
|
|
41
|
+
## Why use the Python helper
|
|
42
|
+
|
|
43
|
+
Many applications only need a local broker to validate the messaging path:
|
|
44
|
+
producers, consumers, subscriptions, flow control, failover, and key-based
|
|
45
|
+
routing. Setting up a full Pulsar deployment can be more expensive than the
|
|
46
|
+
test or prototype itself.
|
|
47
|
+
|
|
48
|
+
`pulsar-lite` keeps the client-facing API close to Pulsar while removing the
|
|
49
|
+
local setup cost:
|
|
50
|
+
|
|
51
|
+
- Start a local broker by passing a file path, no manual process management.
|
|
52
|
+
- Connect with the official `pulsar-client` (installed automatically).
|
|
53
|
+
- Use Pulsar topic names such as `persistent://...` and `non-persistent://...`.
|
|
54
|
+
- Exercise Shared, Failover, Exclusive, and KeyShared subscription behavior.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install pulsar-lite
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The package declares `pulsar-client>=3.0.0` as a runtime dependency, so the
|
|
63
|
+
official Pulsar Python client is installed automatically.
|
|
64
|
+
|
|
65
|
+
Platform wheels include a prebuilt broker binary. If no wheel matches your
|
|
66
|
+
platform, see the [build from source](#building-the-broker-from-source) section.
|
|
67
|
+
|
|
68
|
+
## Quick start: embedded mode
|
|
69
|
+
|
|
70
|
+
Pass a local file path and the helper starts a broker process for you:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import pulsar
|
|
74
|
+
from pulsar_lite import PulsarClient
|
|
75
|
+
|
|
76
|
+
topic = "non-persistent://public/default/quick-start"
|
|
77
|
+
|
|
78
|
+
with PulsarClient("./demo.db") as client:
|
|
79
|
+
consumer = client.subscribe(
|
|
80
|
+
topic,
|
|
81
|
+
"quick-start-sub",
|
|
82
|
+
consumer_type=pulsar.ConsumerType.Shared,
|
|
83
|
+
)
|
|
84
|
+
producer = client.create_producer(topic)
|
|
85
|
+
|
|
86
|
+
producer.send(b"hello from pulsar lite")
|
|
87
|
+
message = consumer.receive(timeout_millis=5000)
|
|
88
|
+
print(message.data().decode("utf-8"))
|
|
89
|
+
consumer.acknowledge(message)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When the `PulsarClient` context exits, the embedded broker process is stopped
|
|
93
|
+
automatically (reference counted, so multiple clients on the same path share
|
|
94
|
+
one broker).
|
|
95
|
+
|
|
96
|
+
## Quick start: remote mode
|
|
97
|
+
|
|
98
|
+
If you already have a broker running (locally or remotely), pass a `pulsar://`
|
|
99
|
+
URI instead and the helper acts as a thin wrapper over `pulsar.Client`:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import pulsar
|
|
103
|
+
from pulsar_lite import PulsarClient
|
|
104
|
+
|
|
105
|
+
with PulsarClient("pulsar://localhost:6650") as client:
|
|
106
|
+
producer = client.create_producer("non-persistent://public/default/events")
|
|
107
|
+
producer.send(b"event-1")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
You can also skip the helper entirely and use the official client directly,
|
|
111
|
+
since the broker speaks the standard Pulsar binary protocol:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import pulsar
|
|
115
|
+
|
|
116
|
+
client = pulsar.Client("pulsar://localhost:6650")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Explicit broker lifecycle
|
|
120
|
+
|
|
121
|
+
For cases where you want to start the broker and then connect with the official
|
|
122
|
+
client (or multiple clients), use `start_broker`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import pulsar
|
|
126
|
+
from pulsar_lite import start_broker
|
|
127
|
+
|
|
128
|
+
with start_broker("./demo.db") as broker:
|
|
129
|
+
client = pulsar.Client(broker.url)
|
|
130
|
+
# ... use the official client against broker.url ...
|
|
131
|
+
client.close()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## API
|
|
135
|
+
|
|
136
|
+
### `PulsarClient(uri, **kwargs)`
|
|
137
|
+
|
|
138
|
+
- `uri`:
|
|
139
|
+
- A local file path (e.g. `"./demo.db"`) starts an embedded broker.
|
|
140
|
+
- A `pulsar://` or `pulsar+ssl://` URI connects to an existing broker.
|
|
141
|
+
- `**kwargs` are forwarded to `pulsar.Client`.
|
|
142
|
+
- Any attribute access not defined on `PulsarClient` itself is forwarded to the
|
|
143
|
+
underlying `pulsar.Client`, so the standard Pulsar API (`create_producer`,
|
|
144
|
+
`subscribe`, `get_topic_partitions`, ...) is fully available.
|
|
145
|
+
- Supports `with` statements and auto-closes the embedded broker on exit.
|
|
146
|
+
|
|
147
|
+
Properties:
|
|
148
|
+
|
|
149
|
+
| Property | Description |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| `is_embedded` | `True` when the helper started a local broker. |
|
|
152
|
+
| `db_path` | The absolute local file path (embedded mode only). |
|
|
153
|
+
| `pulsar_url` | The `pulsar://localhost:<port>` URL the client connects to. |
|
|
154
|
+
|
|
155
|
+
### `start_broker(db_path) -> BrokerHandle`
|
|
156
|
+
|
|
157
|
+
Starts (or reuses) an embedded broker for the given path and returns a
|
|
158
|
+
`BrokerHandle` with `.url`, `.port`, and a `.stop()` method. Use it as a
|
|
159
|
+
context manager for automatic cleanup.
|
|
160
|
+
|
|
161
|
+
## Topic names and subscription modes
|
|
162
|
+
|
|
163
|
+
Pulsar Lite accepts standard Pulsar topic URIs:
|
|
164
|
+
|
|
165
|
+
```text
|
|
166
|
+
persistent://public/default/my-topic
|
|
167
|
+
non-persistent://public/default/my-topic
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Use `non-persistent://...` for live event dispatch where slow or disconnected
|
|
171
|
+
consumers should not create a durable backlog. Use `persistent://...` when a
|
|
172
|
+
test requires stored entries, cursor replay, or acknowledgements across restart
|
|
173
|
+
(requires a broker binary built with RocksDB storage support).
|
|
174
|
+
|
|
175
|
+
Supported subscription modes:
|
|
176
|
+
|
|
177
|
+
| Mode | Summary |
|
|
178
|
+
| --- | --- |
|
|
179
|
+
| Exclusive | One active consumer; additional consumers are rejected. |
|
|
180
|
+
| Failover | One active consumer with standby takeover. |
|
|
181
|
+
| Shared | Messages are distributed across available consumers. |
|
|
182
|
+
| KeyShared | Messages with the same key are routed to the same consumer. |
|
|
183
|
+
|
|
184
|
+
## Binary discovery
|
|
185
|
+
|
|
186
|
+
The helper looks for the bundled broker binary in this order:
|
|
187
|
+
|
|
188
|
+
1. The `pulsar_lite/bin/` directory shipped inside the wheel.
|
|
189
|
+
2. The `PULSAR_LITE_BINARY` environment variable.
|
|
190
|
+
3. The Rust release output at `rust/target/release/pulsar-lite` (development mode).
|
|
191
|
+
4. The system `PATH`.
|
|
192
|
+
5. Common install locations such as `/usr/local/bin/pulsar-lite`.
|
|
193
|
+
|
|
194
|
+
If you build the broker yourself, point `PULSAR_LITE_BINARY` at the resulting
|
|
195
|
+
binary instead of reinstalling the package.
|
|
196
|
+
|
|
197
|
+
## Building the broker from source
|
|
198
|
+
|
|
199
|
+
If no prebuilt wheel matches your platform, build the broker from source and
|
|
200
|
+
let the helper discover it via `PULSAR_LITE_BINARY`:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
git clone https://github.com/ascentstream/pulsar-lite.git
|
|
204
|
+
cd pulsar-lite/rust
|
|
205
|
+
cargo build --release # core build
|
|
206
|
+
cargo build --release --features rocksdb-storage # with persistent storage
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Then install the Python package and point it at your binary:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
cd ../python
|
|
213
|
+
pip install -e .
|
|
214
|
+
export PULSAR_LITE_BINARY=$(pwd)/../rust/target/release/pulsar-lite
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Links
|
|
218
|
+
|
|
219
|
+
- Source: <https://github.com/ascentstream/pulsar-lite>
|
|
220
|
+
- Issues: <https://github.com/ascentstream/pulsar-lite/issues>
|
|
221
|
+
- Full documentation: <https://github.com/ascentstream/pulsar-lite#readme>
|
|
222
|
+
|
|
223
|
+
## Project boundaries
|
|
224
|
+
|
|
225
|
+
Pulsar Lite intentionally does not provide:
|
|
226
|
+
|
|
227
|
+
- Multi-broker coordination or load balancing.
|
|
228
|
+
- Cross-cluster replication.
|
|
229
|
+
- Production-grade authorization or tenant governance.
|
|
230
|
+
- BookKeeper compatibility.
|
|
231
|
+
- Production durability or availability guarantees.
|
|
232
|
+
|
|
233
|
+
The project is useful for local development and compatibility testing, but
|
|
234
|
+
production deployments should use Apache Pulsar.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
Pulsar Lite is licensed under the [Apache License 2.0](https://github.com/ascentstream/pulsar-lite/blob/main/LICENSE).
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/pulsar_lite/__init__.py
|
|
5
|
+
src/pulsar_lite/binary_finder.py
|
|
6
|
+
src/pulsar_lite/client.py
|
|
7
|
+
src/pulsar_lite/process_manager.py
|
|
8
|
+
src/pulsar_lite.egg-info/PKG-INFO
|
|
9
|
+
src/pulsar_lite.egg-info/SOURCES.txt
|
|
10
|
+
src/pulsar_lite.egg-info/dependency_links.txt
|
|
11
|
+
src/pulsar_lite.egg-info/requires.txt
|
|
12
|
+
src/pulsar_lite.egg-info/top_level.txt
|
|
13
|
+
src/pulsar_lite/bin/pulsar-lite
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pulsar_lite
|