Stick to primary after creating composite OAuth token

What

Ai::DuoWorkflows::CreateCompositeOauthAccessTokenService creates an OAuth access token but never registers it with the database load balancer's sticking mechanism. This adds the missing stick_to_database_load_balancer(token) call, mirroring what its sibling Ai::DuoWorkflows::CreateOauthAccessTokenService already does.

Why

The read side, Gitlab::Auth::AuthFinders#find_oauth_access_token, calls load_balancer_stick_request(::OauthAccessToken, :oauth_token, ...). That only forces a primary read when a sticking entry was written at creation time. Without one, each request independently picks a replica. If it picks a replica that has not caught up, OauthAccessToken.by_token returns nil and the request is rejected with 401.

We saw this in a GitLab Duo Agent Platform workload CI job: the Duo CLI's WebSocket upgrade to /api/v4/ai/duo_workflows/ws was rejected with 401, roughly one second after the same token had successfully authenticated against GET /api/v4/version (200, and that endpoint does require authentication) and against /api/graphql. The token existed, was unexpired, and had sufficient scopes, so the 401 was a transient replica-lag miss rather than a permissions or configuration problem.

The composite service has never had sticking, but it was rarely exercised until commit a7ff4969 (2026-08-04) removed the duo_workflow_use_composite_identity feature flag. Since then every Duo Agent Platform CI run goes through Ai::FlowTriggers::RunService#composite_identity_token, so the gap is hit far more often.

Worth noting but out of scope here: the Duo CLI classifies HTTP 401 as AUTH_TOKEN_ERROR, which is on its non-retryable list. A single transient 401 therefore aborts the entire workload with no retries. Making the CLI more forgiving is a separate change.

How to verify

Run the service specs:

bundle exec rspec ee/spec/services/ai/duo_workflows/create_composite_oauth_access_token_service_spec.rb

There is a new example asserting that the created token is stuck to the database load balancer.

References

Edited by Igor Drozdov

Merge request reports

Loading
Loading