Skip to content

Commit 706407e

Browse files
refactor: multiprocess helper class
1 parent 020e935 commit 706407e

3 files changed

Lines changed: 131 additions & 74 deletions

File tree

influxdb_client_3/write_client/client/util/multiprocessing_helper.py

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import logging
88
import multiprocessing
99

10-
from influxdb_client_3.write_client import WriteOptions
10+
from influxdb_client_3 import write_client_options
1111
from influxdb_client_3.exceptions import InfluxDBError
12+
from influxdb_client_3.write_client import WriteOptions, WriteApi
1213

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

@@ -118,17 +119,17 @@ def main():
118119
__started__ = False
119120
__disposed__ = False
120121

121-
def __init__(self, **kwargs) -> None:
122+
def __init__(self, start_method='spawn', **kwargs) -> None:
122123
"""
123124
Initialize defaults.
124125
125-
For more information how to initialize the writer see the examples above.
126+
For more information on how to initialize the writer, see the examples above.
126127
127128
:param kwargs: arguments are passed into ``__init__`` function of ``InfluxDBClient`` and ``write_api``.
128129
"""
130+
multiprocessing.set_start_method(start_method, force=True)
129131
multiprocessing.Process.__init__(self)
130132
self.kwargs = kwargs
131-
self.client = None
132133
self.write_api = None
133134
self.queue_ = multiprocessing.Manager().Queue()
134135

@@ -146,13 +147,22 @@ def write(self, **kwargs) -> None:
146147
self.queue_.put(kwargs)
147148

148149
def run(self):
149-
"""Initialize ``InfluxDBClient`` and waits for data to writes into InfluxDB."""
150+
"""Initialize ``InfluxDBClient3`` and wait for data to write into InfluxDB."""
150151
# Initialize Client and Write API
151-
self.client = InfluxDBClient(**self.kwargs)
152-
self.write_api = self.client.write_api(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))
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+
self.write_api = WriteApi(
159+
bucket=self.kwargs.get('database'),
160+
org=self.kwargs.get('org'),
161+
default_header=self.kwargs.get('default_header'),
162+
rest_client=self.kwargs.get('rest_client'),
163+
write_client_options=wco,
164+
165+
)
156166
# Infinite loop - until poison pill
157167
while True:
158168
next_record = self.queue_.get()
@@ -180,9 +190,6 @@ def terminate(self) -> None:
180190
logger.info("flushing data...")
181191
self.write_api.__del__()
182192
self.write_api = None
183-
if self.client:
184-
self.client.__del__()
185-
self.client = None
186193
logger.info("closed")
187194

188195
def __enter__(self):

influxdb_client_3/write_client/client/write_api.py

Lines changed: 43 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,49 @@ def _should_gzip(self, payload: str, enable_gzip: bool = False, gzip_threshold:
981981

982982
return False
983983

984+
@staticmethod
985+
def _on_error(ex):
986+
logger.error("unexpected error during batching: %s", ex)
987+
988+
def _to_response(self, data: _BatchItem, delay: datetime.timedelta):
989+
return rx.of(data).pipe(
990+
ops.subscribe_on(self._write_options.write_scheduler),
991+
# use delay if its specified
992+
ops.delay(duetime=delay, scheduler=self._write_options.write_scheduler),
993+
# invoke http call
994+
ops.map(lambda x: self._http(x, **x.key.kwargs)),
995+
# catch exception to fail batch response
996+
ops.catch(handler=lambda exception, source: rx.just(_BatchResponse(exception=exception, data=data))),
997+
)
998+
999+
def _on_next(self, response: _BatchResponse):
1000+
if response.exception:
1001+
logger.error("The batch item wasn't processed successfully because: %s", response.exception)
1002+
if self._error_callback:
1003+
try:
1004+
self._error_callback(response.data.to_key_tuple(), response.data.data, response.exception)
1005+
except Exception as e:
1006+
"""
1007+
Unfortunately, because callbacks are user-provided generic code, exceptions can be entirely
1008+
arbitrary
1009+
1010+
We trap it, log that it occurred and then proceed - there's not much more that we can
1011+
really do.
1012+
"""
1013+
logger.error("The configured error callback threw an exception: %s", e)
1014+
1015+
else:
1016+
logger.debug("The batch item: %s was processed successfully.", response)
1017+
if self._success_callback:
1018+
try:
1019+
self._success_callback(response.data.to_key_tuple(), response.data.data)
1020+
except Exception as e:
1021+
logger.error("The configured success callback threw an exception: %s", e)
1022+
1023+
def _on_complete(self):
1024+
self._disposable.dispose()
1025+
logger.debug("the batching processor was disposed")
1026+
9841027
def _append_default_tag(self, key, val, record):
9851028
from influxdb_client_3.write_client import Point
9861029
if isinstance(record, bytes) or isinstance(record, str):
@@ -1065,49 +1108,6 @@ def __getstate__(self):
10651108
del state['_disposable']
10661109
return state
10671110

1068-
@staticmethod
1069-
def _on_error(ex):
1070-
logger.error("unexpected error during batching: %s", ex)
1071-
1072-
def _on_complete(self):
1073-
self._disposable.dispose()
1074-
logger.debug("the batching processor was disposed")
1075-
1076-
def _to_response(self, data: _BatchItem, delay: datetime.timedelta):
1077-
return rx.of(data).pipe(
1078-
ops.subscribe_on(self._write_options.write_scheduler),
1079-
# use delay if its specified
1080-
ops.delay(duetime=delay, scheduler=self._write_options.write_scheduler),
1081-
# invoke http call
1082-
ops.map(lambda x: self._http(x, **x.key.kwargs)),
1083-
# catch exception to fail batch response
1084-
ops.catch(handler=lambda exception, source: rx.just(_BatchResponse(exception=exception, data=data))),
1085-
)
1086-
1087-
def _on_next(self, response: _BatchResponse):
1088-
if response.exception:
1089-
logger.error("The batch item wasn't processed successfully because: %s", response.exception)
1090-
if self._error_callback:
1091-
try:
1092-
self._error_callback(response.data.to_key_tuple(), response.data.data, response.exception)
1093-
except Exception as e:
1094-
"""
1095-
Unfortunately, because callbacks are user-provided generic code, exceptions can be entirely
1096-
arbitrary
1097-
1098-
We trap it, log that it occurred and then proceed - there's not much more that we can
1099-
really do.
1100-
"""
1101-
logger.error("The configured error callback threw an exception: %s", e)
1102-
1103-
else:
1104-
logger.debug("The batch item: %s was processed successfully.", response)
1105-
if self._success_callback:
1106-
try:
1107-
self._success_callback(response.data.to_key_tuple(), response.data.data)
1108-
except Exception as e:
1109-
logger.error("The configured success callback threw an exception: %s", e)
1110-
11111111
def __setstate__(self, state):
11121112
"""Set your object with the provided dict."""
11131113
self.__dict__.update(state)

tests/test_influxdb_client_3_integration.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import asyncio
21
import json
32
import logging
3+
import multiprocessing
44
import os
55
import random
66
import string
77
import time
8+
import asyncio
89
import unittest
910

1011
import pandas as pd
@@ -343,28 +344,77 @@ def test_batch_write_closed(self):
343344
list_results = reader.to_pylist()
344345
self.assertEqual(data_size, len(list_results))
345346

346-
@pytest.mark.skipif(running_on_posix, reason="Skipping this test in POSIX environments")
347+
# @pytest.mark.skipif(running_on_posix, reason="Skipping this test in POSIX environments")
347348
def test_multiprocessing_helper(self):
348-
org = 'my-org'
349-
writer = MultiprocessingWriter(
349+
default_header = {
350+
'Authorization': f'Token {self.token}'
351+
}
352+
rest = rest_client.RestClient(
353+
base_url=self.host,
354+
default_header=default_header,
355+
)
356+
357+
with MultiprocessingWriter(
358+
host=self.host,
359+
database=self.database,
360+
token=self.token,
361+
org='my-org',
362+
default_header=default_header,
363+
rest_client=rest,
364+
write_options=WriteOptions(batch_size=1)) as mp:
365+
self.assertEqual(multiprocessing.get_start_method(), 'spawn')
366+
367+
measurement = f'test{random_hex(6)}'.lower()
368+
for x in range(1, 5):
369+
time.sleep(0.5)
370+
mp.write(
371+
bucket=self.database,
372+
record=f"{measurement},tag=a value=\"number{x}\" {time.time_ns()}"
373+
)
374+
375+
time.sleep(1)
376+
df = self.client.query(f'select * from {measurement}', mode="pandas")
377+
self.assertEqual(4, len(df))
378+
379+
def test_multiprocessing_start_method_forkserver(self):
380+
default_header = {
381+
'Authorization': f'Token {self.token}'
382+
}
383+
384+
MultiprocessingWriter(
350385
host=self.host,
351386
database=self.database,
352387
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__()
388+
org='my-org',
389+
default_header=default_header,
390+
rest_client=(rest_client.RestClient(
391+
base_url=self.host,
392+
default_header=default_header,
393+
)),
394+
write_options=WriteOptions(batch_size=1),
395+
start_method='forkserver'
396+
)
397+
self.assertEqual(multiprocessing.get_start_method(), 'forkserver')
364398

365-
time.sleep(1)
366-
df = self.client.query(f'select * from {measurement}', mode="pandas")
367-
self.assertEqual(9, len(df))
399+
def test_multiprocessing_start_method_fork(self):
400+
default_header = {
401+
'Authorization': f'Token {self.token}'
402+
}
403+
404+
MultiprocessingWriter(
405+
host=self.host,
406+
database=self.database,
407+
token=self.token,
408+
org='my-org',
409+
default_header=default_header,
410+
rest_client=(rest_client.RestClient(
411+
base_url=self.host,
412+
default_header=default_header,
413+
)),
414+
write_options=WriteOptions(batch_size=1),
415+
start_method='fork'
416+
)
417+
self.assertEqual(multiprocessing.get_start_method(), 'fork')
368418

369419
test_cert = """-----BEGIN CERTIFICATE-----
370420
MIIDUzCCAjugAwIBAgIUZB55ULutbc9gy6xLp1BkTQU7siowDQYJKoZIhvcNAQEL

0 commit comments

Comments
 (0)