vtrr-queue 0.2.0__tar.gz → 0.2.1__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.
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/PKG-INFO +18 -5
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/README.md +17 -4
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/pyproject.toml +1 -1
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/queue.py +34 -2
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/tests/test_vtrr.py +86 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/.github/workflows/python-app.yml +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/.github/workflows/python-publish.yml +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/.gitignore +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/CLAUDE.md +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/LICENSE +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/poetry.lock +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/__init__.py +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/py.typed +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/scripts/ack.lua +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/scripts/common.lua +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/scripts/dequeue.lua +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/scripts/enqueue.lua +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/scripts/reclaim.lua +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/src/vtrr_queue/scripts/renew.lua +0 -0
- {vtrr_queue-0.2.0 → vtrr_queue-0.2.1}/tests/conftest.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vtrr-queue
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Virtual-time round-robin fair queue on Redis
|
|
5
5
|
Author-email: Nicolas Johnson <nicolas.johnson@c4ads.org>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -324,7 +324,7 @@ Dequeuing an item does not delete it; it **leases** it. The item moves to an in-
|
|
|
324
324
|
|
|
325
325
|
Delivery is therefore **at-least-once: task bodies must be idempotent.** Inside a task, `self.request.vtrr_timeouts` is how many times this item's lease has expired and been reclaimed: 0 on the first run, +1 each time it is run again after a timeout. The library does not cap timeouts; your task decides when to give up (see [Timeout limits](#timeout-limits)).
|
|
326
326
|
|
|
327
|
-
|
|
327
|
+
A task that raises an exception is not redelivered: the item is acked and finished, as in Celery's default behavior. Only a time limit or a dead worker causes redelivery (see [Retries and timeouts](#retries-and-timeouts)). If you use the low-level API directly:
|
|
328
328
|
|
|
329
329
|
```python
|
|
330
330
|
task_name, args, kwargs, task_id, token, timeouts = vtrr._dequeue()
|
|
@@ -334,10 +334,23 @@ finally:
|
|
|
334
334
|
vtrr.ack(task_id, token) # False if the lease already expired and was re-granted; harmless
|
|
335
335
|
```
|
|
336
336
|
|
|
337
|
-
`self.retry()` / `autoretry_for` keep the lease: the wrapper does not ack when a task retries, and the retry run re-leases the item (expiry reset to `redis_time + lease_seconds`) and acks when it finishes. If the lease was already reclaimed during the retry delay (for example a `countdown` longer than `lease_seconds`), the item belongs to a newer delivery and the retry run is dropped instead of executing twice.
|
|
338
|
-
|
|
339
337
|
`ack` is fenced: it only succeeds when the token matches the current lease, so a slow worker whose lease was reclaimed cannot ack the new holder's lease.
|
|
340
338
|
|
|
339
|
+
### Retries and timeouts
|
|
340
|
+
|
|
341
|
+
| What happens in the task | Result | Counts as a timeout (`vtrr_timeouts`) |
|
|
342
|
+
|---|---|---|
|
|
343
|
+
| Returns normally | Done | No |
|
|
344
|
+
| Raises an exception | Done (failed); not run again | No |
|
|
345
|
+
| Calls `self.retry()`, or raises an exception in `autoretry_for`, with retries left | Run again after the retry delay | No |
|
|
346
|
+
| Raises an `autoretry_for` exception after `max_retries` is used up | Done (failed); the exception is raised | No |
|
|
347
|
+
| Hits `soft_time_limit` (`SoftTimeLimitExceeded`) | Put back in the queue once its lease expires | Yes |
|
|
348
|
+
| Hits the hard `time_limit`, or the worker dies | Put back in the queue once its lease expires | Yes |
|
|
349
|
+
|
|
350
|
+
- **Retries** (`self.retry()` and `autoretry_for`, with `retry_kwargs`, `retry_backoff`, etc.) behave as in plain Celery, and the item keeps its lease while it waits. `self.request.retries` counts them. The retry delay should stay well under `lease_seconds`: if the lease expires first, the retry is dropped and the item is run again as a timeout instead.
|
|
351
|
+
- **Timeouts** are what `self.request.vtrr_timeouts` counts. A timed-out item is re-enqueued with a fresh position in the queue, and the count is never reset.
|
|
352
|
+
- `SoftTimeLimitExceeded` is always treated as a timeout, even if it is listed in `autoretry_for`. It is never counted by `max_retries`.
|
|
353
|
+
|
|
341
354
|
### Celery configuration and timing
|
|
342
355
|
|
|
343
356
|
Recovery does not depend on Celery's delivery guarantees. The Celery message that wakes a worker (the "trigger") carries no payload; the work item lives in Redis under its lease. If a worker dies, the item comes back when its lease expires, and [periodic reclaim](#periodic-reclaim-with-celery-beat) wakes a worker for it. You do not need `acks_late`, `reject_on_worker_lost`, or a raised `visibility_timeout` for this. (They remain available task options. `reject_on_worker_lost` in particular does not speed recovery: the requeued trigger runs while the lease is still held, so it cannot reclaim the item.)
|
|
@@ -358,7 +371,7 @@ def process_file(self, ...):
|
|
|
358
371
|
...
|
|
359
372
|
```
|
|
360
373
|
|
|
361
|
-
- **Hard `time_limit` is required.** `soft_time_limit` only raises an exception, which blocking C/IO code can delay or swallow. The hard limit kills the process, so by the time a lease expires the original holder is gone and reclaim rarely causes concurrent executions.
|
|
374
|
+
- **Hard `time_limit` is required.** `soft_time_limit` only raises an exception, which blocking C/IO code can delay or swallow. The hard limit kills the process, so by the time a lease expires the original holder is gone and reclaim rarely causes concurrent executions. A task that hits either limit is put back in the queue after `lease_seconds` and counted as a timeout.
|
|
362
375
|
- **`lease_seconds` > hard limit**, for the reason above.
|
|
363
376
|
- A recovered item waits for its lease to expire, so worst-case recovery time is about `lease_seconds + reclaim_interval`.
|
|
364
377
|
- If you do enable `acks_late`, the Redis transport's `visibility_timeout` (default 3600s, broker-wide) applies to it: a trigger held unacked longer is redelivered while still running. The lease token fences the duplicate, but keep `visibility_timeout` above your longest task.
|
|
@@ -298,7 +298,7 @@ Dequeuing an item does not delete it; it **leases** it. The item moves to an in-
|
|
|
298
298
|
|
|
299
299
|
Delivery is therefore **at-least-once: task bodies must be idempotent.** Inside a task, `self.request.vtrr_timeouts` is how many times this item's lease has expired and been reclaimed: 0 on the first run, +1 each time it is run again after a timeout. The library does not cap timeouts; your task decides when to give up (see [Timeout limits](#timeout-limits)).
|
|
300
300
|
|
|
301
|
-
|
|
301
|
+
A task that raises an exception is not redelivered: the item is acked and finished, as in Celery's default behavior. Only a time limit or a dead worker causes redelivery (see [Retries and timeouts](#retries-and-timeouts)). If you use the low-level API directly:
|
|
302
302
|
|
|
303
303
|
```python
|
|
304
304
|
task_name, args, kwargs, task_id, token, timeouts = vtrr._dequeue()
|
|
@@ -308,10 +308,23 @@ finally:
|
|
|
308
308
|
vtrr.ack(task_id, token) # False if the lease already expired and was re-granted; harmless
|
|
309
309
|
```
|
|
310
310
|
|
|
311
|
-
`self.retry()` / `autoretry_for` keep the lease: the wrapper does not ack when a task retries, and the retry run re-leases the item (expiry reset to `redis_time + lease_seconds`) and acks when it finishes. If the lease was already reclaimed during the retry delay (for example a `countdown` longer than `lease_seconds`), the item belongs to a newer delivery and the retry run is dropped instead of executing twice.
|
|
312
|
-
|
|
313
311
|
`ack` is fenced: it only succeeds when the token matches the current lease, so a slow worker whose lease was reclaimed cannot ack the new holder's lease.
|
|
314
312
|
|
|
313
|
+
### Retries and timeouts
|
|
314
|
+
|
|
315
|
+
| What happens in the task | Result | Counts as a timeout (`vtrr_timeouts`) |
|
|
316
|
+
|---|---|---|
|
|
317
|
+
| Returns normally | Done | No |
|
|
318
|
+
| Raises an exception | Done (failed); not run again | No |
|
|
319
|
+
| Calls `self.retry()`, or raises an exception in `autoretry_for`, with retries left | Run again after the retry delay | No |
|
|
320
|
+
| Raises an `autoretry_for` exception after `max_retries` is used up | Done (failed); the exception is raised | No |
|
|
321
|
+
| Hits `soft_time_limit` (`SoftTimeLimitExceeded`) | Put back in the queue once its lease expires | Yes |
|
|
322
|
+
| Hits the hard `time_limit`, or the worker dies | Put back in the queue once its lease expires | Yes |
|
|
323
|
+
|
|
324
|
+
- **Retries** (`self.retry()` and `autoretry_for`, with `retry_kwargs`, `retry_backoff`, etc.) behave as in plain Celery, and the item keeps its lease while it waits. `self.request.retries` counts them. The retry delay should stay well under `lease_seconds`: if the lease expires first, the retry is dropped and the item is run again as a timeout instead.
|
|
325
|
+
- **Timeouts** are what `self.request.vtrr_timeouts` counts. A timed-out item is re-enqueued with a fresh position in the queue, and the count is never reset.
|
|
326
|
+
- `SoftTimeLimitExceeded` is always treated as a timeout, even if it is listed in `autoretry_for`. It is never counted by `max_retries`.
|
|
327
|
+
|
|
315
328
|
### Celery configuration and timing
|
|
316
329
|
|
|
317
330
|
Recovery does not depend on Celery's delivery guarantees. The Celery message that wakes a worker (the "trigger") carries no payload; the work item lives in Redis under its lease. If a worker dies, the item comes back when its lease expires, and [periodic reclaim](#periodic-reclaim-with-celery-beat) wakes a worker for it. You do not need `acks_late`, `reject_on_worker_lost`, or a raised `visibility_timeout` for this. (They remain available task options. `reject_on_worker_lost` in particular does not speed recovery: the requeued trigger runs while the lease is still held, so it cannot reclaim the item.)
|
|
@@ -332,7 +345,7 @@ def process_file(self, ...):
|
|
|
332
345
|
...
|
|
333
346
|
```
|
|
334
347
|
|
|
335
|
-
- **Hard `time_limit` is required.** `soft_time_limit` only raises an exception, which blocking C/IO code can delay or swallow. The hard limit kills the process, so by the time a lease expires the original holder is gone and reclaim rarely causes concurrent executions.
|
|
348
|
+
- **Hard `time_limit` is required.** `soft_time_limit` only raises an exception, which blocking C/IO code can delay or swallow. The hard limit kills the process, so by the time a lease expires the original holder is gone and reclaim rarely causes concurrent executions. A task that hits either limit is put back in the queue after `lease_seconds` and counted as a timeout.
|
|
336
349
|
- **`lease_seconds` > hard limit**, for the reason above.
|
|
337
350
|
- A recovered item waits for its lease to expire, so worst-case recovery time is about `lease_seconds + reclaim_interval`.
|
|
338
351
|
- If you do enable `acks_late`, the Redis transport's `visibility_timeout` (default 3600s, broker-wide) applies to it: a trigger held unacked longer is redelivered while still running. The lease token fences the duplicate, but keep `visibility_timeout` above your longest task.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vtrr-queue"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Virtual-time round-robin fair queue on Redis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Nicolas Johnson", email = "nicolas.johnson@c4ads.org" }]
|
|
@@ -4,7 +4,8 @@ import uuid
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Any, Callable
|
|
6
6
|
from celery import Task
|
|
7
|
-
from celery.exceptions import Retry
|
|
7
|
+
from celery.exceptions import Retry, SoftTimeLimitExceeded
|
|
8
|
+
from celery.utils.time import get_exponential_backoff_interval
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
import redis as redis_lib
|
|
@@ -219,6 +220,13 @@ class VTRRQueue:
|
|
|
219
220
|
options = dict(task_options)
|
|
220
221
|
options.pop("bind", None)
|
|
221
222
|
base = _resolve_base(options.pop("base", None))
|
|
223
|
+
# Autoretry is re-implemented by celery_wrapper instead of using Celery's autoretry_for wrapper
|
|
224
|
+
# This is to support retries along with vtrr_queue timeouts/message leasing
|
|
225
|
+
autoretry_for = tuple(options.pop("autoretry_for", ()))
|
|
226
|
+
retry_backoff = float(options.pop("retry_backoff", False))
|
|
227
|
+
retry_backoff_max = int(options.pop("retry_backoff_max", 600))
|
|
228
|
+
retry_jitter = options.pop("retry_jitter", True)
|
|
229
|
+
retry_kwargs = options.pop("retry_kwargs", {})
|
|
222
230
|
task_name = f"{f.__module__}.{f.__name__}"
|
|
223
231
|
|
|
224
232
|
@self._celery_app.task(name=task_name, bind=True, base=base, **options)
|
|
@@ -281,11 +289,35 @@ class VTRRQueue:
|
|
|
281
289
|
registered.name if registered else None,
|
|
282
290
|
)
|
|
283
291
|
return
|
|
284
|
-
|
|
292
|
+
try:
|
|
293
|
+
registered._fn(celery_task, *args, **kwargs)
|
|
294
|
+
except Retry:
|
|
295
|
+
# This exception is only raised by celery if self.retry()
|
|
296
|
+
# was already called by the inner function, so assume the retry task is already scheduled.
|
|
297
|
+
raise
|
|
298
|
+
except SoftTimeLimitExceeded:
|
|
299
|
+
# Treated as a vtrr timeout. Re-raise so terminated=True and message goes through the lease lifecycle.
|
|
300
|
+
raise
|
|
301
|
+
except autoretry_for as exc:
|
|
302
|
+
# Raises Retry (lease kept), or re-raises exc once retries are exhausted.
|
|
303
|
+
auto_retry_kwargs = dict(retry_kwargs)
|
|
304
|
+
if retry_backoff:
|
|
305
|
+
auto_retry_kwargs["countdown"] = (
|
|
306
|
+
get_exponential_backoff_interval(
|
|
307
|
+
factor=int(max(1.0, retry_backoff)),
|
|
308
|
+
retries=celery_task.request.retries,
|
|
309
|
+
maximum=retry_backoff_max,
|
|
310
|
+
full_jitter=retry_jitter,
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
celery_task.retry(exc=exc, **auto_retry_kwargs)
|
|
285
314
|
except Retry:
|
|
286
315
|
# Keep the lease: the retry run re-leases it (renew) and acks when done.
|
|
287
316
|
retrying = True
|
|
288
317
|
raise
|
|
318
|
+
except SoftTimeLimitExceeded:
|
|
319
|
+
terminated = True
|
|
320
|
+
raise
|
|
289
321
|
except BaseException as err:
|
|
290
322
|
# A non-Exception (SystemExit, KeyboardInterrupt, ...) means the process is being
|
|
291
323
|
# terminated, e.g. billiard's SIGTERM at the hard time limit (it raises SystemExit
|
|
@@ -5,6 +5,7 @@ from unittest.mock import MagicMock
|
|
|
5
5
|
|
|
6
6
|
import fakeredis
|
|
7
7
|
import pytest
|
|
8
|
+
from celery.exceptions import SoftTimeLimitExceeded
|
|
8
9
|
|
|
9
10
|
import vtrr_queue.queue as vtrr_module
|
|
10
11
|
from vtrr_queue.queue import VTRRQueue
|
|
@@ -578,6 +579,23 @@ class TestLeases:
|
|
|
578
579
|
assert vtrr.reclaim() == ["t1"]
|
|
579
580
|
assert vtrr._redis.hget(vtrr._timeouts_key, "t1") == b"1"
|
|
580
581
|
|
|
582
|
+
def test_wrapper_does_not_ack_on_soft_time_limit(self, vtrr, monkeypatch):
|
|
583
|
+
"""SoftTimeLimitExceeded is treated like the hard limit: the lease is kept, so it
|
|
584
|
+
expires, the item is reclaimed, and the reclaim counts as a timeout."""
|
|
585
|
+
|
|
586
|
+
@vtrr.task
|
|
587
|
+
def work(self):
|
|
588
|
+
raise SoftTimeLimitExceeded
|
|
589
|
+
|
|
590
|
+
monkeypatch.setattr(vtrr, "_schedule_workers", lambda *a, **k: None)
|
|
591
|
+
work.queue(partition_key="A", tasks=[{"id": "t1"}])
|
|
592
|
+
with pytest.raises(SoftTimeLimitExceeded):
|
|
593
|
+
kick(work)
|
|
594
|
+
assert vtrr._redis.zscore(vtrr._inflight_key, "t1") is not None # still leased
|
|
595
|
+
expire(vtrr, "t1")
|
|
596
|
+
assert vtrr.reclaim() == ["t1"]
|
|
597
|
+
assert vtrr._redis.hget(vtrr._timeouts_key, "t1") == b"1"
|
|
598
|
+
|
|
581
599
|
def test_timeouts_is_visible_to_task(self, vtrr, monkeypatch):
|
|
582
600
|
"""A task run from a first dequeue sees self.request.vtrr_timeouts == 0."""
|
|
583
601
|
seen = []
|
|
@@ -632,6 +650,74 @@ class TestRetryReleasesLease:
|
|
|
632
650
|
assert in_flight == [1, 1] # leased during the original run and the retry
|
|
633
651
|
assert vtrr._redis.zcard(vtrr._inflight_key) == 0 # acked once, at the end
|
|
634
652
|
|
|
653
|
+
def test_autoretry_for_holds_lease_and_acks_at_end(self, vtrr, monkeypatch):
|
|
654
|
+
"""An exception in autoretry_for keeps the lease through the retry, like self.retry()."""
|
|
655
|
+
in_flight = []
|
|
656
|
+
|
|
657
|
+
@vtrr.task(
|
|
658
|
+
autoretry_for=(ConnectionRefusedError,),
|
|
659
|
+
max_retries=2,
|
|
660
|
+
retry_kwargs={"countdown": 0},
|
|
661
|
+
)
|
|
662
|
+
def work(self):
|
|
663
|
+
in_flight.append(vtrr._redis.zcard(vtrr._inflight_key))
|
|
664
|
+
if len(in_flight) == 1:
|
|
665
|
+
raise ConnectionRefusedError
|
|
666
|
+
|
|
667
|
+
monkeypatch.setattr(vtrr, "_schedule_workers", lambda *a, **k: None)
|
|
668
|
+
work.queue(partition_key="A", tasks=[{"id": "t1"}])
|
|
669
|
+
work._celery_task.apply(kwargs={"is_start": True}, throw=False)
|
|
670
|
+
# Work able to run twice and so it was able to re-acquire the lease after the exception.
|
|
671
|
+
assert in_flight == [1, 1]
|
|
672
|
+
assert vtrr._redis.zcard(vtrr._inflight_key) == 0
|
|
673
|
+
|
|
674
|
+
def test_autoretry_for_acks_when_retries_exhausted(self, vtrr, monkeypatch):
|
|
675
|
+
"""Once max_retries is used up the original exception propagates and the item is acked."""
|
|
676
|
+
runs = []
|
|
677
|
+
|
|
678
|
+
@vtrr.task(
|
|
679
|
+
autoretry_for=(ConnectionRefusedError,),
|
|
680
|
+
max_retries=1,
|
|
681
|
+
retry_kwargs={"countdown": 0},
|
|
682
|
+
)
|
|
683
|
+
def work(self):
|
|
684
|
+
runs.append(1)
|
|
685
|
+
raise ConnectionRefusedError
|
|
686
|
+
|
|
687
|
+
monkeypatch.setattr(vtrr, "_schedule_workers", lambda *a, **k: None)
|
|
688
|
+
work.queue(partition_key="A", tasks=[{"id": "t1"}])
|
|
689
|
+
# eager mode runs the retry inline, so the final exception surfaces through apply()
|
|
690
|
+
with pytest.raises(ConnectionRefusedError):
|
|
691
|
+
work._celery_task.apply(kwargs={"is_start": True}, throw=False)
|
|
692
|
+
assert len(runs) == 2 # original + 1 retry
|
|
693
|
+
assert vtrr._redis.zcard(vtrr._inflight_key) == 0
|
|
694
|
+
assert vtrr._redis.hget(vtrr._timeouts_key, "t1") is None
|
|
695
|
+
|
|
696
|
+
def test_soft_time_limit_in_autoretry_for_goes_through_lease_expiry(
|
|
697
|
+
self, vtrr, monkeypatch
|
|
698
|
+
):
|
|
699
|
+
"""SoftTimeLimitExceeded listed in autoretry_for is not Celery-retried: the lease is
|
|
700
|
+
kept until it expires, then the reclaim counts it as a timeout."""
|
|
701
|
+
runs = []
|
|
702
|
+
|
|
703
|
+
@vtrr.task(
|
|
704
|
+
autoretry_for=(SoftTimeLimitExceeded,),
|
|
705
|
+
max_retries=3,
|
|
706
|
+
retry_kwargs={"countdown": 0},
|
|
707
|
+
)
|
|
708
|
+
def work(self):
|
|
709
|
+
runs.append(1)
|
|
710
|
+
raise SoftTimeLimitExceeded
|
|
711
|
+
|
|
712
|
+
monkeypatch.setattr(vtrr, "_schedule_workers", lambda *a, **k: None)
|
|
713
|
+
work.queue(partition_key="A", tasks=[{"id": "t1"}])
|
|
714
|
+
work._celery_task.apply(kwargs={"is_start": True}, throw=False)
|
|
715
|
+
assert len(runs) == 1
|
|
716
|
+
assert vtrr._redis.zscore(vtrr._inflight_key, "t1") is not None
|
|
717
|
+
expire(vtrr, "t1")
|
|
718
|
+
assert vtrr.reclaim() == ["t1"]
|
|
719
|
+
assert vtrr._redis.hget(vtrr._timeouts_key, "t1") == b"1"
|
|
720
|
+
|
|
635
721
|
def test_retry_dropped_when_lease_was_lost(self, vtrr):
|
|
636
722
|
"""A retry run whose token no longer matches any lease (reclaimed, re-granted or
|
|
637
723
|
acked) is dropped without."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|