Add persistent session support with automatic token renewal and retry#156
Merged
Conversation
Owner
|
Good work! I'll consider about the testing anyway. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
By default the library logs in and out on every single database operation, which means a full authentication round-trip each time. The library already supports performing several Data API calls under one PHP request, but this still means a full authentication round-trip each PHP request. This PR adds opt-in support for sharing a session token across PHP requests, so only the first request pays the cost of logging in — every subsequent request on the same server reuses the cached token instead.
This PR adds two features:
sessionCacheconstructor parameter onFMDataAPI.startCommunication(). Without a session cache specified in theFMDataAPIcontructor, it requres explicitely callingsetRetryOnAccessTokenInvalidation(true).The persistent sessions have the retry mechanism enabled by design, as it would not make sense to use persistent sessions without any way of renewing an expired session.
Omitting both the
sessionCacheconstructor parameter andsetRetryOnAccessTokenInvalidation(true)method keeps all existing behavior intact.This PR provides a default cache backend, APCu, for those that have it enabled. It is deliberately not a requirement of the package, and will only work for those that have APCu enabled.
How to use it
Pass a cache backend to the constructor and the library handles the rest — storing the token after login, reusing it on subsequent requests, and automatically recovering if the token goes stale.
New behavior of
startCommunication()andendCommunication()whenever a session cache is specified:startCommunication()— This stored the access token locally as normal. Without this, the request token would be read from the cache on each request, which is still fast.endCommunication()— Same as before, with the addition of being the only location where the cached item's TTL being updated. The details of this choice will be elaborated on further down.The default cache TTL is set to 840 seconds, which leaves a 60 seconds buffer before Data API access token would expire on the FileMaker Server-side.
This PR supports custom cache backends through two different approaches:
AbstractSessionCache(this already implementsSessionCacheInterface, used to reduce boilerplate)SessionCacheInterface.See
ApcuSessionCacheon how to extendAbstractSessionCache. If you want to implementSessionCacheInterfacedirectly, see bothApcuSessionCacheandAbstractSessionCache.New files
AbstractSessionCache.php— public API. Includes properties and setter functions, which reduces boilerplate for custom cache implemetations.ApcuSessionCache.php— public API. Ready-made APCu implementation. Throws if APCu isn't available.SessionCacheInterface.php— public API. Five methods:get,set(with TTL),delete,setTtl,setKey. These will need to be implemented if you want to extendAbstractSessionCache.Changes to existing files
Review carefully
src/Supporting/CommunicationProvider.php— the core of this PR. Most of the new logic lives here.$resumeScopeAfterReauthproperty. This only serves as a way to remember ifkeepAuthwere set before renewing the session through the new retry-mechanism.$retryOnAccessTokenInvalidationproperty. This tells the application if it should retry a failed call if an Data API session token invalidation error occured.$sessionStoreproperty. The instance of the session cache that is used for persistent sessions.startCommunication()(moved) — setskeepAuthto the return value oflogin(), so it's only ever true when login actually succeeded.endCommunication()(moved) — same as the oldendCommunication(), but if persistent session are used: If the cached token matches ours, renews the TTL and skips the server logout. If tokens differ, another worker has replaced the cached token; ours is an orphan and getsDELETEd. Falls through tologout()when no cache is configured. Reasoning further down.login()— checks the cache before attempting an HTTP login. After a fresh login, stores the token in the cache. Changed to callcallRestAPIWithoutRetryinternally.logout()— three new guards: returns early ifaccessTokenis null; returns early if the token matches the cache; wraps theDELETEintry/finally.callRestAPI()/callRestAPIWithoutRetry()— the originalcallRestAPIis renamed tocallRestAPIWithoutRetryand made protected. The newcallRestAPIwraps it with retry logic on 952/112, clearing the cache, re-logging in, and retrying once. The original exception is available viagetPrevious().Mechanical changes — safe to skim
FMDataAPI.php— new$sessionCacheconstructor parameter;startCommunication()andendCommunication()delegate to the provider; newsetRetryOnAccessTokenInvalidation()method.FileMakerLayout.php—startCommunication()andendCommunication()delegate to the provider.TestProvider.php— constructor updated to accept and forwardSessionCacheInterface|nullto the parent.Behavioural changes for existing users
These apply regardless of whether a session cache is configured.
Please verify all of these.
logout()null guard — previously, callinglogout()with no active session would fire aDELETE /sessions/request with a null token in the URL. Before in the case whenthrowExceptionInErrorwas set, this would always throw an exception since this is an invalid operation (it creates a malformed header). Now it immediately returns. This should never occur in the first place, and ifaccessTokenis already null, then we should already be logged out.logout()try/finally —accessTokenis now always nulled after aDELETEattempt, even if the request throws. Previously a thrown exception left it set.startCommunication()login-failure fix — This one were divided into two files. InFMDataAPI.php, thenkeepAuthwere never set tofalsein non-throwing mode. InFileMakerLayout.phpthenkeepAuthwere never set to false in case of a failed login at all. Now, in case of any error, thenkeepAuthis set to false andkeepAuthis only set to true on a successful login.endCommunication()— When a session cache is enabled, this method now only logs out sessions where the localaccessTokendoes not match the cached session token. This is to ensure that one, and only one, session token is stored in the cache. This is also the only place where the TTL of the cached token is renewed.setRetryOnAccessTokenInvalidation(true)to enable it. With a session cache it activates automatically, since a persistent session that can't recover from an expired token is effectively broken. The retry runs in a fresh session — session-scoped state like global fields set before the failure won't carry over.Known limitations
Currently, when a session cache is used, the value stored in the key-value pair only has its TTL renewed upon calling
endCommunication(). This is a deliberate choice as APCu is not atomic and this minimizes the risk of race conditions (rather than setting it after eachcallRestAPI(which increases the surface for race conditions). This means that if an application does not callendCommunication()within 14 minutes from opening the session, then the session will be invalidated in the cache and a new session will be started on the next FileMaker Data API request. This is not an issue, since after 14 minutes, the retry-mechanism catches the invalidated token and retries with a new session.Race conditions and stale tokens
If multiple workers miss the cache simultaneously (on cold start or after a 952 clears it), each logs in independently and gets its own token. Last writer wins the cache; the rest become orphans and idle out within FM Server's 15-minute timeout.
endCommunication()mitigates this by explicitlyDELETEing a token when it finds the cache has already moved on, but workers that crash mid-scope won't get that cleanup. If your FM Server has a low per-user session limit and you anticipate frequent simultaneous cold starts, consider warming the cache from a single entry point before traffic arrives.Under sustained cache failure in
endCommunication()with high concurrency, orphaned tokens could approach FileMaker's session cap (error 953).Regarding tests
I have modified
TestProviderto take in$sessionCacheand replaced thecallRestAPI()override to now overridecallRestAPIWithoutRetry()(as I renamed that function). However, I'm not sure how the tests in this repositories are used, and I'm not sure how to even run them, so I'll leave the testing to you (msyk). If you feel the need to add any tests to this, feel free to do so.Other
As we have already discussed, this PR treats both FileMaker error codes 952 and 112 as a Data API session token invalidation. This was because I could consistently replicate the FileMaker Server returning error code 112 when a Data API session token were invalidated during FileMaker Server Data API request.