From 0bcbe7361b02c4d4c0bb0bb9797f42b6e9a0e5f4 Mon Sep 17 00:00:00 2001 From: Grigory Frolov <2168057+gynsus@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:06:03 +0300 Subject: [PATCH] fix: wait out a concurrent token refresh instead of re-reading a stale row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When RefreshSocialToken and VerifyUpcomingPostConnections fire on the same scheduler tick, the verifier's refreshToken() lost the refresh lock race, immediately re-read the account — before the winning process had saved the new token — and retried verification with the old token, which had just crossed its expiry. The account was then wrongly marked token_expired and disconnected mid-schedule. Block on the lock (up to 15s) so the winner's freshly saved token is what gets re-read. --- app/Services/Social/ConnectionVerifier.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/app/Services/Social/ConnectionVerifier.php b/app/Services/Social/ConnectionVerifier.php index 7c38c91f0..4f33ed542 100644 --- a/app/Services/Social/ConnectionVerifier.php +++ b/app/Services/Social/ConnectionVerifier.php @@ -20,6 +20,7 @@ use App\Services\Social\Discord\DiscordClient; use App\Services\Social\Meta\GraphError; use App\Services\Social\Telegram\TelegramApi; +use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; @@ -204,7 +205,18 @@ public function refreshToken(SocialAccount $account): bool $lock = Cache::lock("token_refresh:{$account->id}", self::REFRESH_LOCK_SECONDS); if (! $lock->get()) { - // Another process is already refreshing this token. + // Another process is already refreshing this token. Wait for it + // to finish before re-reading the row: re-reading immediately + // usually finds the not-yet-persisted old token (both jobs fire + // on the same cron tick) and needlessly fails the caller with a + // transient error even though a fresh token lands moments later. + try { + $lock->block(15); + $lock->release(); + } catch (LockTimeoutException) { + // Fall through — the row is re-read either way. + } + $account->refresh(); if ($account->is_token_expired) {