-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathworkers.py
More file actions
445 lines (375 loc) · 19.1 KB
/
Copy pathworkers.py
File metadata and controls
445 lines (375 loc) · 19.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
"""Segment changes processing worker."""
import logging
import threading
import abc
import gzip
import zlib
import base64
import json
from enum import Enum
from splitio.models.splits import from_raw
from splitio.models.rule_based_segments import from_raw as rbs_from_raw
from splitio.models.telemetry import UpdateFromSSE
from splitio.push import SplitStorageException
from splitio.push.parser import UpdateType
from splitio.optional.loaders import asyncio
from splitio.util.storage_helper import update_feature_flag_storage, update_feature_flag_storage_async, \
update_rule_based_segment_storage, update_rule_based_segment_storage_async
_LOGGER = logging.getLogger(__name__)
class CompressionMode(Enum):
"""Compression modes """
NO_COMPRESSION = 0
GZIP_COMPRESSION = 1
ZLIB_COMPRESSION = 2
_compression_handlers = {
CompressionMode.NO_COMPRESSION: lambda event: base64.b64decode(event.object_definition),
CompressionMode.GZIP_COMPRESSION: lambda event: gzip.decompress(base64.b64decode(event.object_definition)).decode('utf-8'),
CompressionMode.ZLIB_COMPRESSION: lambda event: zlib.decompress(base64.b64decode(event.object_definition)).decode('utf-8'),
}
class WorkerBase(object, metaclass=abc.ABCMeta):
"""Worker template."""
_fetching_segment = "Fetching new segment {segment_name}"
@abc.abstractmethod
def is_running(self):
"""Return whether the working is running."""
@abc.abstractmethod
def start(self):
"""Start worker."""
@abc.abstractmethod
def stop(self):
"""Stop worker."""
def _get_object_definition(self, event):
"""return feature flag or rule based segment definition in event."""
cm = CompressionMode(event.compression) # will throw if the number is not defined in compression mode
return _compression_handlers[cm](event)
def _get_referenced_rbs(self, feature_flag):
referenced_rbs = set()
for condition in feature_flag.conditions:
for matcher in condition.matchers:
raw_matcher = matcher.to_json()
if raw_matcher['matcherType'] == 'IN_RULE_BASED_SEGMENT':
referenced_rbs.add(raw_matcher['userDefinedSegmentMatcherData']['segmentName'])
return referenced_rbs
class SegmentWorker(WorkerBase):
"""Segment Worker for processing updates."""
_centinel = object()
def __init__(self, synchronize_segment, segment_queue):
"""
Class constructor.
:param synchronize_segment: handler to perform segment synchronization on incoming event
:type synchronize_segment: function
:param segment_queue: queue with segment updates notifications
:type segment_queue: queue
"""
self._segment_queue = segment_queue
self._handler = synchronize_segment
self._running = False
self._worker = None
def is_running(self):
"""Return whether the working is running."""
return self._running
def _run(self):
"""Run worker handler."""
while self.is_running():
event = self._segment_queue.get()
if not self.is_running():
break
if event == self._centinel:
continue
_LOGGER.debug('Processing segment_update: %s, change_number: %d',
event.segment_name, event.change_number)
try:
self._handler(event.segment_name, event.change_number)
except Exception:
_LOGGER.error('Exception raised in segment synchronization')
_LOGGER.debug('Exception information: ', exc_info=True)
def start(self):
"""Start worker."""
if self.is_running():
_LOGGER.debug('Worker is already running')
return
self._running = True
_LOGGER.debug('Starting Segment Worker')
self._worker = threading.Thread(target=self._run, name='PushSegmentWorker', daemon=True)
self._worker.start()
def stop(self):
"""Stop worker."""
_LOGGER.debug('Stopping Segment Worker')
if not self.is_running():
_LOGGER.debug('Worker is not running. Ignoring.')
return
self._running = False
self._segment_queue.put(self._centinel)
class SegmentWorkerAsync(WorkerBase):
"""Segment Worker for processing updates."""
_centinel = object()
def __init__(self, synchronize_segment, segment_queue):
"""
Class constructor.
:param synchronize_segment: handler to perform segment synchronization on incoming event
:type synchronize_segment: function
:param segment_queue: queue with segment updates notifications
:type segment_queue: asyncio.Queue
"""
self._segment_queue = segment_queue
self._handler = synchronize_segment
self._running = False
def is_running(self):
"""Return whether the working is running."""
return self._running
async def _run(self):
"""Run worker handler."""
while self.is_running():
event = await self._segment_queue.get()
if not self.is_running():
break
if event == self._centinel:
continue
_LOGGER.debug('Processing segment_update: %s, change_number: %d',
event.segment_name, event.change_number)
try:
await self._handler(event.segment_name, event.change_number)
except Exception:
_LOGGER.error('Exception raised in segment synchronization')
_LOGGER.debug('Exception information: ', exc_info=True)
def start(self):
"""Start worker."""
if self.is_running():
_LOGGER.debug('Worker is already running')
return
self._running = True
_LOGGER.debug('Starting Segment Worker')
asyncio.get_running_loop().create_task(self._run())
async def stop(self):
"""Stop worker."""
_LOGGER.debug('Stopping Segment Worker')
if not self.is_running():
_LOGGER.debug('Worker is not running. Ignoring.')
return
self._running = False
await self._segment_queue.put(self._centinel)
class SplitWorker(WorkerBase):
"""Feature Flag Worker for processing updates."""
_centinel = object()
def __init__(self, synchronize_feature_flag, synchronize_segment, feature_flag_queue, feature_flag_storage, segment_storage, telemetry_runtime_producer, rule_based_segment_storage):
"""
Class constructor.
:param synchronize_feature_flag: handler to perform feature flag synchronization on incoming event
:type synchronize_feature_flag: callable
:param synchronize_segment: handler to perform segment synchronization on incoming event
:type synchronize_segment: function
:param feature_flag_queue: queue with feature flag updates notifications
:type feature_flag_queue: queue
:param feature_flag_storage: feature flag storage instance
:type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage
:param segment_storage: segment storage instance
:type segment_storage: splitio.storage.inmemory.InMemorySegmentStorage
:param telemetry_runtime_producer: Telemetry runtime producer instance
:type telemetry_runtime_producer: splitio.engine.telemetry.TelemetryRuntimeProducer
:param rule_based_segment_storage: Rule based segment Storage.
:type rule_based_segment_storage: splitio.storage.InMemoryRuleBasedStorage
"""
self._feature_flag_queue = feature_flag_queue
self._handler = synchronize_feature_flag
self._segment_handler = synchronize_segment
self._running = False
self._worker = None
self._feature_flag_storage = feature_flag_storage
self._segment_storage = segment_storage
self._telemetry_runtime_producer = telemetry_runtime_producer
self._rule_based_segment_storage = rule_based_segment_storage
def is_running(self):
"""Return whether the working is running."""
return self._running
def _apply_iff_if_needed(self, event):
if not self._check_instant_ff_update(event):
return False
try:
if event.update_type == UpdateType.SPLIT_UPDATE:
new_feature_flag = from_raw(json.loads(self._get_object_definition(event)))
segment_list = update_feature_flag_storage(self._feature_flag_storage, [new_feature_flag], event.change_number)
for segment_name in segment_list:
if self._segment_storage.get(segment_name) is None:
_LOGGER.debug(self._fetching_segment.format(segment_name=segment_name))
self._segment_handler(segment_name, event.change_number)
referenced_rbs = self._get_referenced_rbs(new_feature_flag)
self._fetch_rbs_segment_if_needed(referenced_rbs, event)
self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.SPLIT_UPDATE)
else:
new_rbs = rbs_from_raw(json.loads(self._get_object_definition(event)))
segment_list = update_rule_based_segment_storage(self._rule_based_segment_storage, [new_rbs], event.change_number)
for segment_name in segment_list:
if self._segment_storage.get(segment_name) is None:
_LOGGER.debug(self._fetching_segment.format(segment_name=segment_name))
self._segment_handler(segment_name, event.change_number)
self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.RBS_UPDATE)
return True
except Exception as e:
raise SplitStorageException(e)
def _fetch_rbs_segment_if_needed(self, referenced_rbs, event):
if len(referenced_rbs) > 0 and not self._rule_based_segment_storage.contains(referenced_rbs):
_LOGGER.debug('Fetching new rule based segment(s) %s', referenced_rbs)
self._handler(None, event.change_number)
def _check_instant_ff_update(self, event):
if event.update_type == UpdateType.SPLIT_UPDATE and event.compression is not None and event.previous_change_number == self._feature_flag_storage.get_change_number():
return True
if event.update_type == UpdateType.RB_SEGMENT_UPDATE and event.compression is not None and event.previous_change_number == self._rule_based_segment_storage.get_change_number():
return True
return False
def _run(self):
"""Run worker handler."""
while self.is_running():
event = self._feature_flag_queue.get()
if not self.is_running():
break
if event == self._centinel:
continue
_LOGGER.debug('Processing feature flag update %d', event.change_number)
try:
if self._apply_iff_if_needed(event):
continue
till = None
rbs_till = None
till, rbs_till = self._check_update_type(till, rbs_till, event)
sync_result = self._handler(till, rbs_till)
if not sync_result.success and sync_result.error_code is not None and sync_result.error_code == 414:
_LOGGER.error("URI too long exception caught, sync failed")
if not sync_result.success:
_LOGGER.error("feature flags sync failed")
except SplitStorageException as e: # pylint: disable=broad-except
_LOGGER.error('Exception Updating Feature Flag')
_LOGGER.debug('Exception information: ', exc_info=True)
except Exception as e: # pylint: disable=broad-except
_LOGGER.error('Exception raised in feature flag synchronization')
_LOGGER.debug('Exception information: ', exc_info=True)
def _check_update_type(self, till, rbs_till, event):
if event.update_type == UpdateType.SPLIT_UPDATE:
till = event.change_number
else:
rbs_till = event.change_number
return till, rbs_till
def start(self):
"""Start worker."""
if self.is_running():
_LOGGER.debug('Worker is already running')
return
self._running = True
_LOGGER.debug('Starting Feature Flag Worker')
self._worker = threading.Thread(target=self._run, name='PushFeatureFlagWorker', daemon=True)
self._worker.start()
def stop(self):
"""Stop worker."""
_LOGGER.debug('Stopping Feature Flag Worker')
if not self.is_running():
_LOGGER.debug('Worker is not running')
return
self._running = False
self._feature_flag_queue.put(self._centinel)
class SplitWorkerAsync(WorkerBase):
"""Split Worker for processing updates."""
_centinel = object()
def __init__(self, synchronize_feature_flag, synchronize_segment, feature_flag_queue, feature_flag_storage, segment_storage, telemetry_runtime_producer, rule_based_segment_storage):
"""
Class constructor.
:param synchronize_feature_flag: handler to perform feature_flag synchronization on incoming event
:type synchronize_feature_flag: callable
:param synchronize_segment: handler to perform segment synchronization on incoming event
:type synchronize_segment: function
:param feature_flag_queue: queue with feature_flag updates notifications
:type feature_flag_queue: queue
:param feature_flag_storage: feature flag storage instance
:type feature_flag_storage: splitio.storage.inmemory.InMemorySplitStorage
:param segment_storage: segment storage instance
:type segment_storage: splitio.storage.inmemory.InMemorySegmentStorage
:param telemetry_runtime_producer: Telemetry runtime producer instance
:type telemetry_runtime_producer: splitio.engine.telemetry.TelemetryRuntimeProducer
:param rule_based_segment_storage: Rule based segment Storage.
:type rule_based_segment_storage: splitio.storage.InMemoryRuleBasedStorage
"""
self._feature_flag_queue = feature_flag_queue
self._handler = synchronize_feature_flag
self._segment_handler = synchronize_segment
self._running = False
self._feature_flag_storage = feature_flag_storage
self._segment_storage = segment_storage
self._telemetry_runtime_producer = telemetry_runtime_producer
self._rule_based_segment_storage = rule_based_segment_storage
def is_running(self):
"""Return whether the working is running."""
return self._running
async def _apply_iff_if_needed(self, event):
if not await self._check_instant_ff_update(event):
return False
try:
if event.update_type == UpdateType.SPLIT_UPDATE:
new_feature_flag = from_raw(json.loads(self._get_object_definition(event)))
segment_list = await update_feature_flag_storage_async(self._feature_flag_storage, [new_feature_flag], event.change_number)
for segment_name in segment_list:
if await self._segment_storage.get(segment_name) is None:
_LOGGER.debug(self._fetching_segment.format(segment_name=segment_name))
await self._segment_handler(segment_name, event.change_number)
referenced_rbs = self._get_referenced_rbs(new_feature_flag)
await self._fetch_rbs_segment_if_needed(referenced_rbs, event)
await self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.SPLIT_UPDATE)
else:
new_rbs = rbs_from_raw(json.loads(self._get_object_definition(event)))
segment_list = await update_rule_based_segment_storage_async(self._rule_based_segment_storage, [new_rbs], event.change_number)
for segment_name in segment_list:
if await self._segment_storage.get(segment_name) is None:
_LOGGER.debug(self._fetching_segment.format(segment_name=segment_name))
await self._segment_handler(segment_name, event.change_number)
await self._telemetry_runtime_producer.record_update_from_sse(UpdateFromSSE.RBS_UPDATE)
return True
except Exception as e:
raise SplitStorageException(e)
async def _fetch_rbs_segment_if_needed(self, referenced_rbs, event):
if len(referenced_rbs) > 0 and not await self._rule_based_segment_storage.contains(referenced_rbs):
_LOGGER.debug('Fetching new rule based segment(s) %s', referenced_rbs)
await self._handler(None, event.change_number)
async def _check_instant_ff_update(self, event):
if event.update_type == UpdateType.SPLIT_UPDATE and event.compression is not None and event.previous_change_number == await self._feature_flag_storage.get_change_number():
return True
if event.update_type == UpdateType.RB_SEGMENT_UPDATE and event.compression is not None and event.previous_change_number == await self._rule_based_segment_storage.get_change_number():
return True
return False
async def _run(self):
"""Run worker handler."""
while self.is_running():
event = await self._feature_flag_queue.get()
if not self.is_running():
break
if event == self._centinel:
continue
_LOGGER.debug('Processing split_update %d', event.change_number)
try:
if await self._apply_iff_if_needed(event):
continue
till = None
rbs_till = None
if event.update_type == UpdateType.SPLIT_UPDATE:
till = event.change_number
else:
rbs_till = event.change_number
await self._handler(till, rbs_till)
except SplitStorageException as e: # pylint: disable=broad-except
_LOGGER.error('Exception Updating Feature Flag')
_LOGGER.debug('Exception information: ', exc_info=True)
except Exception as e: # pylint: disable=broad-except
_LOGGER.error('Exception raised in split synchronization')
_LOGGER.debug('Exception information: ', exc_info=True)
def start(self):
"""Start worker."""
if self.is_running():
_LOGGER.debug('Worker is already running')
return
self._running = True
_LOGGER.debug('Starting Split Worker')
asyncio.get_running_loop().create_task(self._run())
async def stop(self):
"""Stop worker."""
_LOGGER.debug('Stopping Split Worker')
if not self.is_running():
_LOGGER.debug('Worker is not running')
return
self._running = False
await self._feature_flag_queue.put(self._centinel)