fix: resolve OAuth re-auth deadlock and sync concurrency races (#88)
* fix(gtasks): prevent concurrent push/pull from racing on the store Problem: `push` and `pull` both run via `oauth.async`, so issuing them back-to-back starts two coroutines that interleave at every curl yield. Both snapshot `build_id_index` before either has mutated the store, which can cause push to create a remote task that pull would have recognized as already linked, producing duplicates on Google. Solution: guard `with_token` with a module-level `_in_flight` flag set before `oauth.async` is called so no second operation can start during a token-refresh yield. A `pcall` around the callback guarantees the flag is always cleared, even on an unexpected error. * refactor(sync): centralize `with_token` in oauth.lua with shared lock Problem: `with_token` was duplicated in `gcal.lua` and `gtasks.lua`, with the concurrency lock added only to the gtasks copy. Any new backend would silently inherit the same race, and gcal back-to-back push could still create duplicate remote calendar events. Solution: lift `with_token` into `oauth.lua` as `M.with_token(client, name, callback)` behind a module-level `_sync_in_flight` guard. All backends share one implementation; the lock covers gcal, gtasks, and any future backend automatically. * ci: format
This commit is contained in:
parent
b641c93a0a
commit
874ff381f9
3 changed files with 29 additions and 28 deletions
|
|
@ -169,20 +169,8 @@ local function fmt_counts(parts)
|
|||
return table.concat(items, ' | ')
|
||||
end
|
||||
|
||||
---@param callback fun(access_token: string): nil
|
||||
local function with_token(callback)
|
||||
oauth.async(function()
|
||||
local token = oauth.google_client:get_access_token()
|
||||
if not token then
|
||||
log.warn('not authenticated — run :Pending auth')
|
||||
return
|
||||
end
|
||||
callback(token)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.push()
|
||||
with_token(function(access_token)
|
||||
oauth.with_token(oauth.google_client, 'gcal', function(access_token)
|
||||
local calendars, cal_err = get_all_calendars(access_token)
|
||||
if cal_err or not calendars then
|
||||
log.error(cal_err or 'failed to fetch calendars')
|
||||
|
|
|
|||
|
|
@ -436,20 +436,8 @@ local function sync_setup(access_token)
|
|||
return tasklists, s, now_ts
|
||||
end
|
||||
|
||||
---@param callback fun(access_token: string): nil
|
||||
local function with_token(callback)
|
||||
oauth.async(function()
|
||||
local token = oauth.google_client:get_access_token()
|
||||
if not token then
|
||||
log.warn('not authenticated — run :Pending auth')
|
||||
return
|
||||
end
|
||||
callback(token)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.push()
|
||||
with_token(function(access_token)
|
||||
oauth.with_token(oauth.google_client, 'gtasks', function(access_token)
|
||||
local tasklists, s, now_ts = sync_setup(access_token)
|
||||
if not tasklists then
|
||||
return
|
||||
|
|
@ -475,7 +463,7 @@ function M.push()
|
|||
end
|
||||
|
||||
function M.pull()
|
||||
with_token(function(access_token)
|
||||
oauth.with_token(oauth.google_client, 'gtasks', function(access_token)
|
||||
local tasklists, s, now_ts = sync_setup(access_token)
|
||||
if not tasklists then
|
||||
return
|
||||
|
|
@ -502,7 +490,7 @@ function M.pull()
|
|||
end
|
||||
|
||||
function M.sync()
|
||||
with_token(function(access_token)
|
||||
oauth.with_token(oauth.google_client, 'gtasks', function(access_token)
|
||||
local tasklists, s, now_ts = sync_setup(access_token)
|
||||
if not tasklists then
|
||||
return
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ local OAuthClient = {}
|
|||
OAuthClient.__index = OAuthClient
|
||||
|
||||
local _active_close = nil
|
||||
local _sync_in_flight = false
|
||||
|
||||
---@class pending.oauth
|
||||
local M = {}
|
||||
|
|
@ -51,6 +52,30 @@ function M.async(fn)
|
|||
coroutine.resume(coroutine.create(fn))
|
||||
end
|
||||
|
||||
---@param client pending.OAuthClient
|
||||
---@param name string
|
||||
---@param callback fun(access_token: string): nil
|
||||
function M.with_token(client, name, callback)
|
||||
if _sync_in_flight then
|
||||
require('pending.log').warn(name .. ': sync operation in progress — please wait')
|
||||
return
|
||||
end
|
||||
_sync_in_flight = true
|
||||
M.async(function()
|
||||
local token = client:get_access_token()
|
||||
if not token then
|
||||
_sync_in_flight = false
|
||||
require('pending.log').warn(name .. ': not authenticated — run :Pending auth')
|
||||
return
|
||||
end
|
||||
local ok, err = pcall(callback, token)
|
||||
_sync_in_flight = false
|
||||
if not ok then
|
||||
require('pending.log').error(name .. ': ' .. tostring(err))
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return string
|
||||
function M.url_encode(str)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue