Skip to content

Do not re-send message pieces that were already accepted when retrying - #1779

Merged
caronc merged 2 commits into
caronc:masterfrom
SulimanAbdulrazzaq:fix/retry-skip-pieces-already-accepted
Oct 14, 2026
Merged

caronc merged 2 commits into
caronc:masterfrom
SulimanAbdulrazzaq:fix/retry-skip-pieces-already-accepted

Conversation

@SulimanAbdulrazzaq

Copy link
Copy Markdown
Contributor

Description

Related issue (if applicable): none, completes the retry feature from #1739

With retry= set, a message that overflows into several pieces sends the already accepted pieces again when a later piece fails. call_with_retry() in apprise/dispatch.py calls service.notify() for the whole service on every attempt, and NotifyBase.notify() sends every piece through _timed_send(). The delivery tracker (is_delivered() / mark_delivered()) is opt-in per plugin, so a plugin that does not mark anything, such as json://, gets every piece re-sent. #1739 says a retry should only retry what failed.

Reproducer with mocks only (nothing is sent): json://localhost/?retry=1&wait=0&overflow=split, a 32883 character body that splits into three requests, and the mock answers 500 for the request containing PIECE2 on the first pass only.

Before:

['PIECE1', 'filler', 'PIECE2(500)', 'PIECE1', 'filler', 'PIECE2']

After:

['PIECE1', 'filler', 'PIECE2(500)', 'PIECE2']

_timed_send() now does two things for the piece it is sending:

  • before calling the sender, return True without sending if the piece was already accepted
  • after the sender returns a truthy result, mark the piece as accepted
if self.is_delivered(_PIECE_DELIVERED):
    ...
    return True

result = send_fn(**kwargs)
if result:
    self.mark_delivered(_PIECE_DELIVERED)

The key is a module constant, so it does not collide with the keys a plugin tracks its own targets under. Both calls do nothing when retries are off (is_delivered() returns False and mark_delivered() has no tracker), so a notification without retry= is sent exactly as before. A piece is marked only when the whole piece returned True, so a piece that reached only some of its targets is still retried and the plugin's own tracking decides who receives it again. async_notify() goes through _timed_send() too, so it gets the same behavior.

Checklist

  • Documentation ticket created (if applicable): not applicable, no documented syntax changes
  • The change is tested and works locally.
  • No commented-out code in this PR.
  • No lint errors (use tox -e lint and optionally tox -e format).
  • Test coverage added or updated (use tox -e qa).

Testing

Four tests in tests/test_delivery_tracking.py use json:// with overflow=split, a body that splits into three pieces, and a mock where the last piece is refused:

  • test_accepted_pieces_are_not_resent_on_retry: with retry=1, the first two pieces are sent once, the last one twice (refused, then accepted) and the notification succeeds
  • test_accepted_pieces_are_sent_once_without_retry: with retry=0 every piece is sent once and the failure is final (control, unchanged behavior)
  • test_failing_piece_exhausts_retries_without_resending_others: a piece that is refused on every attempt exhausts retry=2, the notification fails, and the first two pieces are still sent once
  • test_async_accepted_pieces_are_not_resent_on_retry: the same as the first test through async_notify()

Without the change in base.py the first, third and fourth tests fail (3 failed, 42 passed in tests/test_delivery_tracking.py); with it all 45 pass.

pip install git+https://github.com/caronc/apprise.git@fix/retry-skip-pieces-already-accepted
pytest tests/test_delivery_tracking.py -q

Not included: per-target tracking in plugins (unchanged).

@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.99%. Comparing base (f746ec5) to head (f9af2de).
⚠ïļ Report is 9 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff             @@
##            master    #1779      +/-   ##
===========================================
- Coverage   100.00%   99.99%   -0.01%     
===========================================
  Files          246      246              
  Lines        40326    40298      -28     
  Branches      6956     6953       -3     
===========================================
- Hits         40326    40297      -29     
- Partials         0        1       +1     
Files with missing lines Coverage Δ
apprise/plugins/base.py 100.00% <100.00%> (Ãļ)

... and 11 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄ïļ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@caronc
caronc merged commit 1350024 into caronc:master Oct 14, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants