Skip to content

Commit 3b01cd0

Browse files
refactor: multiprocess helper class
1 parent b24d6c1 commit 3b01cd0

5 files changed

Lines changed: 107 additions & 74 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
- `debug` - boolean - toggle debug level logging.
2626
- Any clients explicitly using the `Configuration`, `ApiClient`, `WriteService` or _legacy_ `InfluxDBClient` classes, will need to migrate their settings to the `InfluxDBClient3` constructor.
2727

28+
1. [#222](https://github.com/InfluxCommunity/influxdb3-python/pull/222): Refactor `MultiprocessingWriter` class.
29+
- New Process will be created by using `DefaultContext.Process(target)` to prevent erratic crashes, handle cross-platform code behavior safely, and coordinate complex resource sharing.
30+
- Users can now choose one of the start methods `fork`, `spawn` or `forkserver` when creating new Process. The default will be `spawn`.
31+
2832
## 0.20.0 [2026-06-11]
2933

3034
### Features

codecov.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
coverage:
2+
status:
3+
project:
4+
default:
5+
target: auto
6+
removed_code_behavior: fully_covered_patch
7+
threshold: 1%

influxdb_client_3/write_client/client/util/multiprocessing_helper.py

Lines changed: 50 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
import logging
88
import multiprocessing
99

10-
from influxdb_client_3 import InfluxDBClient3, write_client_options
11-
from influxdb_client_3.write_client import WriteOptions
10+
from influxdb_client_3 import write_client_options
1211
from influxdb_client_3.exceptions import InfluxDBError
12+
from influxdb_client_3.write_client import WriteOptions, WriteApi
13+
from influxdb_client_3.write_client._sync import rest_client
1314

1415
logger = logging.getLogger('influxdb_client.client.util.multiprocessing_helper')
1516

@@ -35,9 +36,9 @@ class _PoisonPill:
3536
pass
3637

3738

38-
class MultiprocessingWriter(multiprocessing.Process):
39+
class MultiprocessingWriter:
3940
"""
40-
The Helper class to write data into InfluxDB in independent OS process.
41+
The Helper class to write data into InfluxDB in an independent OS process.
4142
4243
Example:
4344
.. code-block:: python
@@ -119,83 +120,80 @@ def main():
119120
__started__ = False
120121
__disposed__ = False
121122

122-
def __init__(self, **kwargs) -> None:
123+
def __init__(self, start_method='spawn', **kwargs) -> None:
123124
"""
124125
Initialize defaults.
125126
126-
For more information how to initialize the writer see the examples above.
127+
For more information on how to initialize the writer, see the examples above.
127128
128-
:param kwargs: arguments are passed into ``__init__`` function of ``InfluxDBClient`` and ``write_api``.
129+
:param kwargs: arguments are passed into the `` _ _init__`` function of ``InfluxDBClient`` and ``write_api``.
129130
"""
130-
multiprocessing.Process.__init__(self)
131+
132+
wco = write_client_options(write_options=kwargs.get('write_options', WriteOptions()),
133+
success_callback=kwargs.get('success_callback', _success_callback),
134+
error_callback=kwargs.get('error_callback', _error_callback),
135+
retry_callback=kwargs.get('retry_callback', _retry_callback)
136+
)
137+
138+
if kwargs.get('rest_client') is not None:
139+
rest = kwargs.get('rest_client')
140+
else:
141+
token = kwargs.get('token')
142+
default_header = {'Authorization': f'Token {token}'}
143+
rest = rest_client.RestClient(
144+
base_url=kwargs.get('host'),
145+
default_header=default_header,
146+
)
147+
148+
write_api = WriteApi(
149+
bucket=kwargs.get('database'),
150+
org=kwargs.get('org'),
151+
default_header=kwargs.get('default_header'),
152+
rest_client=rest,
153+
**wco
154+
)
155+
156+
self.ctx = multiprocessing.get_context(start_method)
157+
self.process = self.ctx.Process(target=self.run, args=(write_api,))
131158
self.kwargs = kwargs
132-
self.client = None
133-
self.write_api = None
134-
self.queue_ = multiprocessing.Manager().Queue()
159+
self.queue_ = self.ctx.JoinableQueue()
135160

136161
def write(self, **kwargs) -> None:
137162
"""
138-
Append time-series data into underlying queue.
163+
Append time-series data into the underlying queue.
139164
140-
For more information how to pass arguments see the examples above.
165+
For more information on how to pass arguments, see the examples above.
141166
142-
:param kwargs: arguments are passed into ``write`` function of ``WriteApi``
167+
:param kwargs: arguments are passed into the `` write `` function of ``WriteApi``
143168
:return: None
144169
"""
145170
assert self.__disposed__ is False, 'Cannot write data: the writer is closed.'
146171
assert self.__started__ is True, 'Cannot write data: the writer is not started.'
147172
self.queue_.put(kwargs)
148173

149-
def run(self):
174+
def run(self, write_api: WriteApi) -> None:
150175
"""Initialize ``InfluxDBClient3`` and wait for data to write into InfluxDB."""
151-
# Initialize Client and Write API
152-
wco = write_client_options(write_options=self.kwargs.get('write_options', WriteOptions()),
153-
success_callback=self.kwargs.get('success_callback', _success_callback),
154-
error_callback=self.kwargs.get('error_callback', _error_callback),
155-
retry_callback=self.kwargs.get('retry_callback', _retry_callback)
156-
)
157-
158-
# Still need to create the InfluxDBClient3 because the init logics of InfluxDBClient3 will create the WriteApi.
159-
# it will make WriteApi class created properly.
160-
self.client = InfluxDBClient3(write_client_options=wco, **self.kwargs)
161176

162-
# Close and set _query_api to None because query_api is not needed in this process.
163-
# We only need write_api.
164-
self.client._query_api.close()
165-
self.client._query_api = None
166-
167-
self.write_api = self.client._write_api
168177
# Infinite loop - until poison pill
169178
while True:
170179
next_record = self.queue_.get()
171180
if type(next_record) is _PoisonPill:
172181
# Poison pill means break the loop
173-
self.terminate()
182+
logger.info("flushing data...")
183+
write_api.close()
184+
logger.info("closed")
174185
self.queue_.task_done()
175186
break
176-
self.write_api.write(**next_record)
187+
write_api.write(**next_record)
177188
self.queue_.task_done()
178189

179190
def start(self) -> None:
180-
"""Start independent process for writing data into InfluxDB."""
181-
super().start()
191+
"""Start an independent process for writing data into InfluxDB."""
192+
self.process.start()
182193
self.__started__ = True
183194

184-
def terminate(self) -> None:
185-
"""
186-
Cleanup resources in independent process.
187-
188-
This function **cannot be used** to terminate the ``MultiprocessingWriter``.
189-
If you want to finish your writes please call: ``__del__``.
190-
"""
191-
if self.write_api:
192-
logger.info("flushing data...")
193-
self.write_api.__del__()
194-
self.write_api = None
195-
if self.client:
196-
self.client.close()
197-
self.client = None
198-
logger.info("closed")
195+
def get_start_processing_method(self):
196+
return self.ctx.get_start_method()
199197

200198
def __enter__(self):
201199
"""Enter the runtime context related to this object."""
@@ -207,11 +205,11 @@ def __exit__(self, exc_type, exc_value, traceback):
207205
self.__del__()
208206

209207
def __del__(self):
210-
"""Dispose the client and write_api."""
208+
"""Dispose of the client and write_api."""
211209
if self.__started__:
212210
self.queue_.put(_PoisonPill())
213211
self.queue_.join()
214-
self.join()
212+
self.process.join()
215213
self.queue_ = None
216214
self.__started__ = False
217215
self.__disposed__ = True

pyproject.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
[build-system]
22
requires = ["setuptools>=82.0.1"]
3-
build-backend = "setuptools.build_meta"
3+
build-backend = "setuptools.build_meta"
4+
5+
[tool.coverage.run]
6+
concurrency = ["multiprocessing"]
7+
parallel = true
8+
source = ["influxdb_client_3.write_client.client.util"]

tests/test_influxdb_client_3_integration.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import asyncio
12
import json
23
import logging
34
import os
45
import random
56
import string
67
import time
7-
import asyncio
88
import unittest
99

1010
import pandas as pd
@@ -21,8 +21,6 @@
2121
from influxdb_client_3.write_client.write_exceptions import ApiException
2222
from tests.util import asyncio_run, lp_to_py_object
2323

24-
running_on_posix = os.name == 'posix'
25-
2624

2725
def random_hex(len=6):
2826
return ''.join(random.choice(string.hexdigits) for i in range(len))
@@ -343,28 +341,49 @@ def test_batch_write_closed(self):
343341
list_results = reader.to_pylist()
344342
self.assertEqual(data_size, len(list_results))
345343

346-
@pytest.mark.skipif(running_on_posix, reason="Skipping this test in POSIX environments")
347344
def test_multiprocessing_helper(self):
348-
org = 'my-org'
349-
writer = MultiprocessingWriter(
350-
host=self.host,
351-
database=self.database,
352-
token=self.token,
353-
org=org,
354-
write_options=WriteOptions(batch_size=1))
355-
writer.start()
356-
measurement = f'test{random_hex(6)}'.lower()
357-
for x in range(1, 10):
358-
time.sleep(0.2)
359-
writer.write(
360-
bucket=self.database,
361-
record=f"{measurement},tag=a value=\"number{x}\" {time.time_ns()}"
362-
)
363-
writer.__del__()
345+
with MultiprocessingWriter(
346+
host=self.host,
347+
database=self.database,
348+
token=self.token,
349+
org='my-org',
350+
write_options=WriteOptions(batch_size=1)) as mp:
351+
self.assertEqual(mp.get_start_processing_method(), 'spawn')
352+
353+
measurement = f'test{random_hex(6)}'.lower()
354+
for x in range(1, 5):
355+
time.sleep(0.5)
356+
mp.write(
357+
bucket=self.database,
358+
record=f"{measurement},tag=a value=\"number{x}\" {time.time_ns()}"
359+
)
364360

365361
time.sleep(1)
366362
df = self.client.query(f'select * from {measurement}', mode="pandas")
367-
self.assertEqual(9, len(df))
363+
self.assertEqual(4, len(df))
364+
365+
def test_multiprocessing_start_method_forkserver(self):
366+
with MultiprocessingWriter(
367+
host=self.host,
368+
database=self.database,
369+
token=self.token,
370+
org='my-org',
371+
write_options=WriteOptions(batch_size=1),
372+
start_method='forkserver'
373+
) as mp:
374+
self.assertEqual(mp.get_start_processing_method(), 'forkserver')
375+
376+
def test_multiprocessing_start_method_fork(self):
377+
378+
with MultiprocessingWriter(
379+
host=self.host,
380+
database=self.database,
381+
token=self.token,
382+
org='my-org',
383+
write_options=WriteOptions(batch_size=1),
384+
start_method='fork'
385+
) as mp:
386+
self.assertEqual(mp.get_start_processing_method(), 'fork')
368387

369388
test_cert = """-----BEGIN CERTIFICATE-----
370389
MIIDUzCCAjugAwIBAgIUZB55ULutbc9gy6xLp1BkTQU7siowDQYJKoZIhvcNAQEL

0 commit comments

Comments
 (0)