Make Server thread-safe with per-thread sessions - #1871
Open
dxdc wants to merge 1 commit into
Open
Conversation
|
Thanks for the contribution! Before we can merge this, we need @dxdc to sign the Salesforce Inc. Contributor License Agreement. |
Author
|
signed the cla |
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.
Motivation
Audit result from #1148: all HTTP traffic flows through a single requests.Session stored on Server, and requests.Session is not guaranteed to be thread-safe (psf/requests#2766 - connection pool and cookie state can be corrupted under concurrent use). Auth state (_auth_token, _site_id, _user_id, _site_url) is also written field-by-field with no lock, so a concurrent reader could observe a token paired with the wrong site during switch_site or re-sign-in.
The practical consequence was that the common pattern of sharing one Server across a ThreadPoolExecutor (e.g. bulk workbook downloads) was unsafe, and the workaround was one fully signed-in Server per thread.
Behavior change
Server.sessionnow lazily returns a per-threadrequests.Sessioncreated fromsession_factory, cached inthreading.local. This is exactly the "one session per thread" guidance from the requests maintainers, applied transparently. Thread pool workers reuse their session (and its connection pool) across tasks._set_auth/_clear_authare guarded by a lock so the (site, user, token) triple is always updated atomically. Reads stay lock-free._clear_authbumps the epoch, and each thread lazily replaces its cached session on next use. In-flight requests on other threads are not disrupted, matching the old re-assignment semantics.Server.close()and context-manager support (with TSC.Server(...) as server:). Sessions created for any thread are tracked in aWeakSet(weak, so sessions of exited threads can still be garbage collected) andclose()closes them all, releasing pooled connections.close()is transport-level only - it does not sign out - so it composes with the existingauth.sign_in()context manager, which already handles sign-out. The Server remains usable afterclose(); the epoch bump means any later call creates fresh sessions instead of hitting closed pools.Serverdocstring: share one instance freely, sign in (and calluse_server_version()) before spawning workers, and note thatsession_factorymay now be called once per thread.No public API changes. All endpoint code already routed through the
sessionproperty, so the change is confined toserver.py. The one private-surface change is thatServer._sessionno longer exists as an attribute.New
test/test_thread_safety.py:test_each_thread_gets_its_own_session- N threads held live behind a barrier each see a distinct, per-thread-stable session objecttest_session_factory_called_once_per_thread- factory runs exactly once per thread despite repeatedsessionaccesstest_sign_out_invalidates_sessions_of_all_threads- afterauth.sign_out(), both the signing-out thread and a still-alive worker thread get fresh sessionstest_concurrent_api_calls_use_per_thread_sessions- 8-workerThreadPoolExecutormaking 40 mockedusers.get()calls; every request completes and carries the shared auth tokentest_auth_state_is_set_atomically- reader threads hammerauth_token/site_idwhile the main thread cycles_set_auth/_clear_auth; no reader ever observes a mismatched token/site pairtest_close_closes_sessions_of_all_threads-close()closes the sessions of the constructing thread and all workers, and the server remains usable afterwardstest_close_does_not_sign_out- auth token survivesclose()test_context_manager_closes_on_exit-__enter__returns the server,__exit__closes sessionsVerified the first two tests fail against the current
developmentbranch.