Skip to content

fix(loopback): circles:check always fails due to APCu race and missing CLI wrapper#2387

Open
i2h3 wants to merge 2 commits intomasterfrom
i2h3/fix/9690491-circles-loopback
Open

fix(loopback): circles:check always fails due to APCu race and missing CLI wrapper#2387
i2h3 wants to merge 2 commits intomasterfrom
i2h3/fix/9690491-circles-loopback

Conversation

@i2h3
Copy link

@i2h3 i2h3 commented Mar 6, 2026

Summary

occ circles:check always reports an invalid loopback address even when the address is correct. Two independent bugs prevent the check from ever succeeding:

  • APCu cache race condition causes the POST to asyncBroadcast with test-dummy-token to return HTTP 401
  • CLI guard in initBroadcast() prevents EventWrapper creation for LoopbackTest events, causing the subsequent async verification to find zero wrappers

Both bugs are fixed in separate commits for clarity.

Bug 1: APCu cache race condition (401 on POST)

testLoopback() first makes a GET request to core.CSRFToken.index, then inserts test_dummy_token into appconfig, then makes a POST to circles.EventWrapper.asyncBroadcast.

The GET request causes the web server to load all appconfig values into its APCu local cache (TTL = 3 seconds). The CLI process then inserts test_dummy_token into the database and clears its own APCu cache — but CLI and web server run under different SAPI contexts with independent APCu stores, so the web server's cache is unaffected. When the POST request arrives (well within the 3-second TTL), the web process reads from its still-valid APCu cache, which does not contain the newly inserted key. IAppConfig::getValueInt() returns the default 0, and 0 < time() evaluates to true, so the asyncBroadcast controller returns 401.

Fix: Move setValueInt() to before the GET request so the key exists in the database before any web request populates the APCu cache.

Bug 2: CLI guard prevents wrapper creation (0 wrappers)

Even after the 401 is resolved, circles:check fails with "Event created too many Wrappers" (actually zero). initBroadcast() has an early-return guard:

if (empty($instances) && (!$event->isAsync() || OC::$CLI)) {
    return false;
}

For a LoopbackTest event (implements IFederatedItemAsyncProcess, no remote instances), $instances is empty. Combined with OC::$CLI === true, the guard returns false — skipping all wrapper creation. This optimisation is correct for regular async events in CLI mode where newEvent() already calls manage() synchronously. However, LoopbackTest specifically needs the full async path (wrapper creation → async POST → manageWrapper()confirmStatus()) to verify that the loopback works end-to-end.

Fix: Exempt events whose class implements IFederatedItemLoopbackTest from the CLI guard. The double call to manage() (once synchronously, once via the async wrapper) is harmless for LoopbackTest since it only sets an idempotent result value.

Steps to reproduce

docker run --name test-circles --detach --publish 8085:80 \
  --env SQLITE_DATABASE=nextcloud.sqlite \
  --env NEXTCLOUD_ADMIN_PASSWORD=admin \
  --env NEXTCLOUD_ADMIN_USER=admin \
  nextcloud:32.0.6

docker exec -u www-data test-circles php occ circles:check

Before fix

- GET request on http://localhost/csrftoken: 200
- POST request on http://localhost/apps/circles/async/test-dummy-token/: 401
- You do not have a valid loopback address setup right now.

After fix

- GET request on http://localhost/csrftoken: 200
- POST request on http://localhost/apps/circles/async/test-dummy-token/: 200
- Creating async FederatedEvent 51a9a7e3-... (took 16ms)
- Waiting for async process to finish (5s)
- Checking status on FederatedEvent verify=17 manage=42
* Loopback address looks good

Test plan

  • Run occ circles:check on a fresh Nextcloud installation — loopback check should pass
  • Run occ circles:check repeatedly — should pass consistently (no intermittent 401)
  • Verify normal async event processing (circle creation, member operations) is unaffected by the initBroadcast change
  • Verify the test_dummy_token TTL (10 seconds) is not exceeded between setValueInt and the POST request under normal conditions

🤖 Generated with Claude Code

i2h3 and others added 2 commits March 6, 2026 15:00
The `circles:check` loopback test always fails with a 401 on the POST
request to `asyncBroadcast` with `test-dummy-token` when the config key
`test_dummy_token` does not already exist in the database.

Root cause: The GET request to `core.CSRFToken.index` (which runs first)
causes the web server process to load all `appconfig` values into its
APCu local cache (TTL=3s). Only then does the CLI process insert
`test_dummy_token` into the database and clear its own APCu cache — but
CLI and web server run in separate SAPI contexts with independent APCu
stores, so the web cache is unaffected. When the POST request arrives
shortly after the GET (well within the 3-second TTL), the web process
reads from its still-valid APCu cache, which does not contain the newly
inserted key. `getValueInt()` therefore returns the default `0`, and
`0 < time()` evaluates to true, causing the controller to return 401.

Moving `setValueInt()` before the GET request ensures the key exists in
the database before any web request loads the appconfig into APCu.

Signed-Off-By: Iva Horn <iva.horn@nextcloud.com>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Even after the 401 is resolved, `circles:check` still fails with
"Event created too many Wrappers" (actually zero wrappers found, the
message is misleading). The LoopbackTest FederatedEvent never gets an
EventWrapper created when running from CLI.

Root cause: `initBroadcast()` has an early-return guard:

    if (empty($instances) && (!$event->isAsync() || OC::$CLI))

For a LoopbackTest event (which implements IFederatedItemAsyncProcess but
has no remote instances), `$instances` is empty. Combined with
`OC::$CLI === true`, the guard returns false — skipping all wrapper
creation. This optimisation is correct for regular async events in CLI
mode, where `newEvent()` already calls `manage()` synchronously and no
async broadcast is needed. However, the LoopbackTest specifically exists
to verify that the async loopback path works end-to-end: wrapper
creation, async POST, `manageWrapper()`, and `confirmStatus()` must all
run to produce the verify/manage values that `testLoopback()` asserts.

The fix exempts events whose class implements IFederatedItemLoopbackTest
from the CLI guard, so that a wrapper is created and the async broadcast
is triggered even when running from CLI. The double call to `manage()`
(once synchronously in `newEvent()`, once asynchronously via the
wrapper) is harmless for LoopbackTest since it only sets an idempotent
result value.

Signed-Off-By: Iva Horn <iva.horn@nextcloud.com>

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes two independent bugs that caused occ circles:check to always report an invalid loopback address, even when the address is correct.

Changes:

  • APCu race condition fix (CirclesCheck.php): Moves setValueInt() for test_dummy_token to before the GET request, ensuring the key is in the database before any web request can populate the APCu cache. This prevents the web server from caching a database snapshot that lacks the newly inserted key, which previously caused the subsequent asyncBroadcast POST to return HTTP 401.
  • CLI guard bypass for loopback tests (FederatedEventService.php): Exempts events whose class implements IFederatedItemLoopbackTest from the early-return guard in initBroadcast() that skips async wrapper creation when running under CLI. This allows the LoopbackTest event to go through the full async path (wrapper creation → async POST → manageWrapper()) needed to verify end-to-end loopback functionality.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
lib/Command/CirclesCheck.php Reorders setValueInt before the GET request to avoid APCu cache race condition
lib/Service/FederatedEventService.php Adds IFederatedItemLoopbackTest exemption to the CLI guard in initBroadcast() to allow async wrapper creation for loopback test events

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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