nucliadb-utils 6.1.0.post2589__py3-none-any.whl → 6.1.0.post2602__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.

Potentially problematic release.


This version of nucliadb-utils might be problematic. Click here for more details.

nucliadb_utils/nats.py CHANGED
@@ -19,6 +19,7 @@
19
19
 
20
20
  import asyncio
21
21
  import logging
22
+ import sys
22
23
  import time
23
24
  from functools import cached_property
24
25
  from typing import Any, Awaitable, Callable, Optional, Union
@@ -126,6 +127,9 @@ class NatsConnectionManager:
126
127
  self._lock = asyncio.Lock()
127
128
  self._healthy = True
128
129
  self._last_unhealthy: Optional[float] = None
130
+ self._needs_reconnection = False
131
+ self._reconnect_task: Optional[asyncio.Task] = None
132
+ self._expected_subscriptions: set[str] = set()
129
133
 
130
134
  def healthy(self) -> bool:
131
135
  if not self._healthy:
@@ -159,8 +163,23 @@ class NatsConnectionManager:
159
163
  async with self._lock:
160
164
  self._nc = await nats.connect(**options)
161
165
 
166
+ self._expected_subscription_task = asyncio.create_task(self._verify_expected_subscriptions())
167
+
162
168
  async def finalize(self):
163
169
  async with self._lock:
170
+ if self._reconnect_task:
171
+ self._reconnect_task.cancel()
172
+ try:
173
+ await self._reconnect_task
174
+ except asyncio.CancelledError:
175
+ pass
176
+
177
+ self._expected_subscription_task.cancel()
178
+ try:
179
+ await self._expected_subscription_task
180
+ except asyncio.CancelledError:
181
+ pass
182
+
164
183
  # Finalize push subscriptions
165
184
  for sub, _ in self._subscriptions:
166
185
  try:
@@ -199,42 +218,75 @@ class NatsConnectionManager:
199
218
  logger.warning(
200
219
  f"Reconnected to NATS {self._nc.connected_url.netloc}. Attempting to re-subscribe."
201
220
  )
202
- async with self._lock:
203
- existing_subs = self._subscriptions
204
- self._subscriptions = []
205
- for sub, recon_callback in existing_subs:
206
- try:
207
- await sub.drain()
208
- await recon_callback()
209
- except Exception:
210
- logger.exception(
211
- f"Error resubscribing to {sub.subject} on {self._nc.connected_url.netloc}"
212
- )
213
- # should force exit here to restart the service
214
- self._healthy = False
215
- raise
216
-
217
- existing_pull_subs = self._pull_subscriptions
218
- self._pull_subscriptions = []
219
- for pull_sub, task, recon_callback, cancelled in existing_pull_subs:
220
- cancelled.set()
221
- task.cancel()
222
- try:
223
- await task
224
- except asyncio.CancelledError:
225
- pass
226
- try:
227
- await pull_sub.unsubscribe()
228
- await recon_callback()
229
- except Exception:
230
- logger.exception(
231
- f"Error resubscribing to pull subscription on {self._nc.connected_url.netloc}"
232
- )
233
- # should force exit here to restart the service
234
- self._healthy = False
235
- raise
236
- self._healthy = True
237
- self._last_unhealthy = None # reset the last unhealthy time
221
+ # This callback may be interrupted by NATS. In order to avoid being interrupted, we spawn our own task to do
222
+ # the reconnection. If we are interrupted we could lose subscriptions
223
+ self._needs_reconnection = True
224
+ if self._reconnect_task is None:
225
+ self._reconnect_task = asyncio.create_task(self._reconnect())
226
+
227
+ async def _reconnect(self):
228
+ tries = 0
229
+ # Loop in case we receive another reconnection request while one is ongoing
230
+ while self._needs_reconnection:
231
+ if tries > 5:
232
+ logger.error(
233
+ "Too many consecutive reconnection to NATS. Something might be wrong. Exiting."
234
+ )
235
+ sys.exit(1)
236
+
237
+ try:
238
+ self._needs_reconnection = False
239
+
240
+ async with self._lock:
241
+ existing_subs = self._subscriptions
242
+ self._subscriptions = []
243
+ for sub, recon_callback in existing_subs:
244
+ try:
245
+ await sub.drain()
246
+ await recon_callback()
247
+ except Exception:
248
+ logger.exception(
249
+ f"Error resubscribing to {sub.subject} on {self._nc.connected_url.netloc}"
250
+ )
251
+ # should force exit here to restart the service
252
+ self._healthy = False
253
+ raise
254
+
255
+ existing_pull_subs = self._pull_subscriptions
256
+ self._pull_subscriptions = []
257
+ for pull_sub, task, recon_callback, cancelled in existing_pull_subs:
258
+ cancelled.set()
259
+ task.cancel()
260
+ try:
261
+ await task
262
+ except asyncio.CancelledError:
263
+ pass
264
+ try:
265
+ await pull_sub.unsubscribe()
266
+ await recon_callback()
267
+ except Exception:
268
+ logger.exception(
269
+ f"Error resubscribing to pull subscription on {self._nc.connected_url.netloc}"
270
+ )
271
+ # should force exit here to restart the service
272
+ self._healthy = False
273
+ raise
274
+ self._healthy = True
275
+ self._last_unhealthy = None # reset the last unhealthy time
276
+ except Exception:
277
+ if self._needs_reconnection:
278
+ logger.warning("Error reconnecting to NATS, will retry")
279
+ else:
280
+ logger.exception("Error reconnecting to NATS. Exiting.")
281
+ sys.exit(1)
282
+
283
+ if self._needs_reconnection:
284
+ logger.warning(
285
+ "While reconnecting to NATS subscriptions, a reconnect request was received. Reconnecting again.",
286
+ )
287
+ tries += 1
288
+
289
+ self._reconnect_task = None
238
290
 
239
291
  async def error_cb(self, e): # pragma: no cover
240
292
  logger.error(f"There was an error on consumer: {e}", exc_info=e)
@@ -314,6 +366,10 @@ class NatsConnectionManager:
314
366
 
315
367
  task = asyncio.create_task(consume(psub, subject), name=f"pull_subscribe_{subject}")
316
368
  self._pull_subscriptions.append((psub, task, subscription_lost_cb, cancelled))
369
+
370
+ if durable:
371
+ self._expected_subscriptions.add(durable)
372
+
317
373
  return psub
318
374
 
319
375
  async def _remove_subscription(
@@ -340,3 +396,23 @@ class NatsConnectionManager:
340
396
  async def unsubscribe(self, subscription: Union[Subscription, JetStreamContext.PullSubscription]):
341
397
  await subscription.unsubscribe()
342
398
  await self._remove_subscription(subscription)
399
+
400
+ if isinstance(subscription, JetStreamContext.PullSubscription):
401
+ self._expected_subscriptions.remove(subscription._consumer) # type: ignore
402
+
403
+ async def _verify_expected_subscriptions(self):
404
+ failures = 0
405
+ while True:
406
+ await asyncio.sleep(30)
407
+
408
+ existing_subs = set(sub._consumer for sub, _, _, _ in self._pull_subscriptions)
409
+ missing_subs = self._expected_subscriptions - existing_subs
410
+ if missing_subs:
411
+ logger.warning(f"Some NATS subscriptions are missing {missing_subs}")
412
+ failures += 1
413
+ else:
414
+ failures = 0
415
+
416
+ if failures >= 3:
417
+ logger.warning("Some NATS subscriptions are missing for too long, exiting")
418
+ sys.exit(1)
@@ -92,6 +92,8 @@ def get_utility(ident: Union[Utility, str]):
92
92
 
93
93
 
94
94
  def set_utility(ident: Union[Utility, str], util: Any):
95
+ if ident in MAIN:
96
+ logger.warning(f"Overwriting previously set utility {ident}: {MAIN[ident]} with {util}")
95
97
  MAIN[ident] = util
96
98
 
97
99
 
@@ -237,6 +239,10 @@ def get_ingest() -> WriterStub:
237
239
 
238
240
 
239
241
  def start_partitioning_utility() -> PartitionUtility:
242
+ util = get_utility(Utility.PARTITION)
243
+ if util is not None:
244
+ return util
245
+
240
246
  util = PartitionUtility(
241
247
  partitions=nuclia_settings.nuclia_partitions,
242
248
  seed=nuclia_settings.nuclia_hash_seed,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: nucliadb_utils
3
- Version: 6.1.0.post2589
3
+ Version: 6.1.0.post2602
4
4
  Home-page: https://nuclia.com
5
5
  License: BSD
6
6
  Classifier: Development Status :: 4 - Beta
@@ -24,8 +24,8 @@ Requires-Dist: PyNaCl
24
24
  Requires-Dist: pyjwt>=2.4.0
25
25
  Requires-Dist: memorylru>=1.1.2
26
26
  Requires-Dist: mrflagly>=0.2.9
27
- Requires-Dist: nucliadb-protos>=6.1.0.post2589
28
- Requires-Dist: nucliadb-telemetry>=6.1.0.post2589
27
+ Requires-Dist: nucliadb-protos>=6.1.0.post2602
28
+ Requires-Dist: nucliadb-telemetry>=6.1.0.post2602
29
29
  Provides-Extra: cache
30
30
  Requires-Dist: redis>=4.3.4; extra == "cache"
31
31
  Requires-Dist: orjson>=3.6.7; extra == "cache"
@@ -8,7 +8,7 @@ nucliadb_utils/featureflagging.py,sha256=dKp8u4g52KTcupKFJkdGYk3Z2pGQdi_q7IaoY_G
8
8
  nucliadb_utils/grpc.py,sha256=apu0uePnkGHCAT7GRQ9YZfRYyFj26kJ440i8jitbM3U,3314
9
9
  nucliadb_utils/helpers.py,sha256=nPw8yod3hP-pxq80VF8QC36s7ygSg0dBUdfI-LatvCs,1600
10
10
  nucliadb_utils/indexing.py,sha256=Luaqcar3CySpdYOFp6Q9Fyr8ZYwhYhaKRHQ_VGL78f8,2318
11
- nucliadb_utils/nats.py,sha256=SGBWtP0h3_9Ne6VEfZ2OLnWlEvzfAj63U252FrTITjY,11870
11
+ nucliadb_utils/nats.py,sha256=YsSuhGKpZjyZXjVT7uAF6KjRMWvp1k7-SF9ddFXdVC0,15059
12
12
  nucliadb_utils/partition.py,sha256=jBgy4Hu5Iwn4gjbPPcthSykwf-qNx-GcLAIwbzPd1d0,1157
13
13
  nucliadb_utils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  nucliadb_utils/run.py,sha256=Es0_Bu5Yc-LWczvwL6gzWqSwC85RjDCk-0oFQAJi9g4,1827
@@ -16,7 +16,7 @@ nucliadb_utils/settings.py,sha256=BOuYTh08cJe7UgBgU9ALHCJm6eDezbdPsdAXM4HGEgo,81
16
16
  nucliadb_utils/signals.py,sha256=JRNv2y9zLtBjOANBf7krGfDGfOc9qcoXZ6N1nKWS2FE,2674
17
17
  nucliadb_utils/store.py,sha256=kQ35HemE0v4_Qg6xVqNIJi8vSFAYQtwI3rDtMsNy62Y,890
18
18
  nucliadb_utils/transaction.py,sha256=GjM754TU9940pfQY3vxAQAYlQdBpqfwrO-_gYkSjWrI,7979
19
- nucliadb_utils/utilities.py,sha256=vO6NUQxFpljTo7y6TemoOewMsXLgKTJmVPTtCWnjsDk,15921
19
+ nucliadb_utils/utilities.py,sha256=_5pNsXBknULGrTvV4hgIjsOJ0sSbFq4ygseH89C1gdk,16128
20
20
  nucliadb_utils/aiopynecone/__init__.py,sha256=cp15ZcFnHvpcu_5-aK2A4uUyvuZVV_MJn4bIXMa20ks,835
21
21
  nucliadb_utils/aiopynecone/client.py,sha256=MPyHnDXwhukJr7U3CJh7BpsekfSuOkyM4g5b9LLtzc8,22941
22
22
  nucliadb_utils/aiopynecone/exceptions.py,sha256=fUErx3ceKQK1MUbOnYcZhIzpNe8UVAptZE9JIRDLXDE,4000
@@ -59,8 +59,8 @@ nucliadb_utils/tests/indexing.py,sha256=YW2QhkhO9Q_8A4kKWJaWSvXvyQ_AiAwY1VylcfVQ
59
59
  nucliadb_utils/tests/local.py,sha256=fXIBasrvdaFJM-sw2wk1_oiFzBcm9O10iCyC-OiXwY8,1914
60
60
  nucliadb_utils/tests/nats.py,sha256=xqpww4jZjTKY9oPGlJdDJG67L3FIBQsa9qDHxILR8r8,7687
61
61
  nucliadb_utils/tests/s3.py,sha256=pl-RJFjA4MH6iXkqhsh5g8gDuEhrYu1nPZ-laxlrMlE,3704
62
- nucliadb_utils-6.1.0.post2589.dist-info/METADATA,sha256=qrXQ8HGQZ1FFhwMMAbyclo6eGgBvxlFXssBqqMAtZ4I,2055
63
- nucliadb_utils-6.1.0.post2589.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
64
- nucliadb_utils-6.1.0.post2589.dist-info/top_level.txt,sha256=fE3vJtALTfgh7bcAWcNhcfXkNPp_eVVpbKK-2IYua3E,15
65
- nucliadb_utils-6.1.0.post2589.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
66
- nucliadb_utils-6.1.0.post2589.dist-info/RECORD,,
62
+ nucliadb_utils-6.1.0.post2602.dist-info/METADATA,sha256=7CB6_mYBRnxvaeq3fcpYyAvr62mVWpZCyhdHcT_CvLY,2055
63
+ nucliadb_utils-6.1.0.post2602.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
64
+ nucliadb_utils-6.1.0.post2602.dist-info/top_level.txt,sha256=fE3vJtALTfgh7bcAWcNhcfXkNPp_eVVpbKK-2IYua3E,15
65
+ nucliadb_utils-6.1.0.post2602.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
66
+ nucliadb_utils-6.1.0.post2602.dist-info/RECORD,,