Skip to content

fix(sync): prevent client sync stalls from empty PING responses and undrained backlogs - #103

Merged
ralflang merged 2 commits into
FRAMEWORK_6_0from
fix/ping-stale-response-backlog-recovery
Aug 26, 2026
Merged

ralflang merged 2 commits into
FRAMEWORK_6_0from
fix/ping-stale-response-backlog-recovery

Conversation

@TDannhauer

Copy link
Copy Markdown
Contributor

Summary

  • PING no longer returns an empty HTTP 200 body when a parallel request
    invalidated the sync cache (COLLECTION_ERR_STALE); it now emits a valid
    Status-1 WBXML response as required by MS-ASCMD.
  • The server now self-heals abandoned windowed SYNC drains: if a collection's
    last response shipped MOREAVAILABLE and the client never returned, PING
    reports the collection as changed so the client resumes its SYNC loop.

Motivation

Addresses #102. Device logs show a repeating stall pattern with Nine for
Android: a transient connection error aborts the client's windowed SYNC loop
mid-drain, the subsequent PING gets an empty 200 response (interpreted as a
dead connection, triggering back-off), and once the client finally re-PINGs,
the server correctly-but-unhelpfully reports "no changes" because the ping
watermark only tracks new activity — the undrained backlog stays stuck until
unrelated new mail arrives.

Changes

  • Request/Ping.php: fall through to the normal response path on
    COLLECTION_ERR_STALE instead of returning without output.
  • SyncCache.php: new per-collection backlog flag with timestamp and trigger
    counter (setBacklogFlag, resetBacklogFlag, hasStaleBacklog,
    getBacklogPingCount, incrementBacklogPingCount).
  • Collections.php: pollForChanges() reports a collection as changed when
    its backlog flag is older than BACKLOG_GRACE_PERIOD (60s), capped at
    BACKLOG_TRIGGER_MAX (3) triggers per flag. An actively draining client
    refreshes the flag with every windowed response and is never affected.
  • Request/Sync.php: set the backlog flag on windowed/over-window responses,
    clear it when a sync completes without pending changes; reset
    $forceChanges per collection iteration (previously leaked across
    collections).

Expected exception types in new code paths: none added; existing
Horde_ActiveSync_Exception handling is unchanged.

Test plan

  • PingStaleCacheResponseTest: stale-cache PING produces a non-empty
    WBXML body containing Status 1 (real encoder/decoder round-trip).
  • BacklogPingRecoveryTest: backlog flag lifecycle, grace period
    behavior, and trigger cap.
  • Full ActiveSync unit suite: no new failures (3 pre-existing failures
    unrelated to this change).
  • Verified on a live deployment against Nine: stalled folders resume
    syncing after the next PING cycle.

When a parallel request modified the sync cache during a PING
(COLLECTION_ERR_STALE), the handler exited without emitting any WBXML,
returning HTTP 200 with a 0-byte body. MS-ASCMD requires every PING
response to carry a status, and some clients (e.g. Nine) treat the
empty body as a dead connection, back off, and stall their sync loop.
Fall through to the normal response path so the client receives
STATUS_NOCHANGES and re-issues the PING cleanly.
When a windowed SYNC response ships MOREAVAILABLE but the client never
returns to continue draining (e.g. its sync loop died on a transient
connection error), the PING watermark only tracks new activity and
never reports the old backlog, so the pending items stay undelivered
until unrelated new mail arrives.

Track a per-collection backlog flag in the sync cache whenever a
response is windowed, clear it when a SYNC completes without pending
changes, and let pollForChanges report the collection as changed when
the flag has not been refreshed for BACKLOG_GRACE_PERIOD seconds.
A BACKLOG_TRIGGER_MAX cap prevents a stale flag from producing an
endless PING/SYNC loop; the counter resets on every windowed response.

Also reset $forceChanges per collection iteration in the Sync handler;
it previously leaked from one collection to the next.
@TDannhauer
TDannhauer requested a review from ralflang August 2, 2026 14:03
@TDannhauer

Copy link
Copy Markdown
Contributor Author

@ralflang : ready for review and merge

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

🔍 CI Results

Overall: ❌ 12/12 lanes failed

TL;DR: ❌ Quality issues: PHPStan: 243 unique errors in 11 lanes; PHP-CS-Fixer: 31 files.

Summary by PHP Version

PHP dev stable
8.0 ❌ ❌
8.1 ❌ ❌
8.2 ❌ ❌
8.3 ❌ ❌
8.4 ❌ ❌
8.5 ❌ ❌

Quality Metrics

  • PHPUnit: did not run (11 lanes exited non-zero with no test output) ❌
  • PHPStan (advisory): 243 unique errors in 11 lanes ⚠️
  • PHP-CS-Fixer: 31 unique files with issues (of 213 checked) ⚠️
❌ Failed Lanes

php8.0-dev

  • PHPUnit: 0 failures, 0 errors

php8.0-stable

  • PHPUnit: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)
  • PHPStan: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)

php8.1-dev

  • PHPUnit: 0 failures, 0 errors

php8.1-stable

  • PHPUnit: 0 failures, 0 errors

php8.2-dev

  • PHPUnit: 0 failures, 0 errors

php8.2-stable

  • PHPUnit: 0 failures, 0 errors

php8.3-dev

  • PHPUnit: 0 failures, 0 errors

php8.3-stable

  • PHPUnit: 0 failures, 0 errors

php8.4-dev

  • PHPUnit: 0 failures, 0 errors
  • PHP-CS-Fixer: 31 files with issues

php8.4-stable

  • PHPUnit: 0 failures, 0 errors

php8.5-dev

  • PHPUnit: 0 failures, 0 errors

php8.5-stable

  • PHPUnit: 0 failures, 0 errors

CI powered by horde-components • View full results

@TDannhauer

Copy link
Copy Markdown
Contributor Author

Ping @ralflang

1 similar comment
@TDannhauer

Copy link
Copy Markdown
Contributor Author

Ping @ralflang

@ralflang
ralflang merged commit 261a448 into FRAMEWORK_6_0 Aug 26, 2026
1 check failed
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