# How DS2API Implements Account Pool Rotation with Token Refresh

> Learn how DS2API implements account pool rotation with a round-robin queue and automatically refreshes tokens using time-based checks. Discover efficient token management for your API.

- Repository: [CJACK./ds2api](https://github.com/CJackHwang/ds2api)
- Tags: how-to-guide
- Published: 2026-04-26

---

**DS2API implements account pool rotation using a round-robin queue mechanism in `internal/account` while automatically refreshing tokens via time-based checks in [`internal/auth/request.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/auth/request.go) based on a configurable interval.**

DS2API (`CJackHwang/ds2api`) distributes API requests across multiple managed DeepSeek/OpenAI accounts using a sophisticated pooling system. The architecture separates account lifecycle management from authentication logic, ensuring both **account pool rotation** and **token refresh** occur transparently to API callers. This implementation guarantees fair load distribution while maintaining valid credentials through proactive renewal.

## The Account Pool Rotation Mechanism

The rotation system centers on a thread-safe pool that distributes accounts using a round-robin strategy. According to the source code in `internal/account`, the pool maintains a queue of account identifiers and tracks which accounts are currently in use.

### Pool Initialization and Account Sorting

When the pool starts, `NewPool` creates the structure and immediately invokes `Reset()` to prepare the rotation queue. As implemented in `internal/account/pool_core.go:22-71`, the `Reset` method performs several critical setup steps:

- Sorts the underlying accounts so those **with existing tokens** appear first, prioritizing ready-to-use credentials.
- Constructs a **queue** of account identifiers that determines the rotation order.
- Records the **recommended concurrency** and rate limits for each account slot.

This initialization ensures the pool begins with a clear, deterministic order while respecting account capabilities.

### Round-Robin Acquisition with bumpQueue

When a request needs an account, it calls `Acquire` or `AcquireWait`, both of which lock the pool and delegate to `acquireLocked`. Located in `internal/account/pool_acquire.go:49-80`, the acquisition logic scans the `queue` for the first available identifier that is not excluded or already in use.

Once an account is selected, the pool performs two atomic operations in `internal/account/pool_acquire.go:83-91`:

1. Increments `inUse[id]` to mark the account as active.
2. Calls `bumpQueue(id)` to move the identifier to the **end of the queue**.

This `bumpQueue` operation implements the round-robin rotation. The next request receives the subsequent account in the queue, ensuring traffic spreads evenly across all configured credentials rather than hammering the same account repeatedly.

### Safe Release and Concurrency Management

After request completion, `Release` decrements the in-use counter for the specific account ID. As shown in `internal/account/pool_core.go:83-100`, when the count reaches zero, the slot becomes available and the method notifies any waiting goroutines via the pool's condition variable. This mutex-protected flow prevents race conditions while maintaining high throughput across concurrent API calls.

## Token Refresh Strategy and Implementation

While the pool handles rotation, the `internal/auth` package ensures tokens remain valid. The resolver implements a time-based refresh strategy that checks credentials before each request and automatically renews expired tokens.

### Configurable Refresh Intervals

The system reads the refresh cadence from configuration field `runtime.token_refresh_interval_hours`, defined in `internal/config/config.go:63-68`. The default interval is **6 hours**, though operators can adjust this based on the backend provider's token lifetime requirements.

### The Token Freshness Check

The authentication resolver maintains a `tokenRefreshedAt` map in `internal/auth/request.go:43-46` that tracks the last successful refresh timestamp for each account ID. Before using an acquired account, the code calls `ensureManagedToken` (`internal/auth/request.go:50-62`), which implements the following decision tree:

- If the account's token string is **empty**, trigger `loginAndPersist`.
- If `shouldForceRefresh` returns **true** (implemented in `internal/auth/request.go:64-84`), compare the elapsed time since `tokenRefreshedAt[id]` against the configured interval; if expired, trigger `loginAndPersist`.
- Otherwise, reuse the existing cached token.

The `shouldForceRefresh` method calculates whether `time.Now().Sub(lastRefresh) >= interval`, providing a thread-safe check protected by the resolver's mutex.

### Automatic Login and Persistence

When refresh is required, `loginAndPersist` (located in `internal/auth/request.go:50-60`) executes the injected `Login` function to obtain a new token from the upstream provider. Upon success, it:

- Stores the new token on the account object.
- Calls `markTokenRefreshedNow` (`internal/auth/request.go:86-92`) to record `time.Now()` in the `tokenRefreshedAt` map.
- Updates the persistent store to survive restarts.

If a token is explicitly invalidated (for example, after a 401 response), `MarkTokenInvalid` removes the account's entry from the refresh map (`internal/auth/request.go:74-82`), forcing a fresh login on the next request.

## Complete Request Flow Example

The following Go snippet illustrates how these systems interact during a typical API request:

```go
// Step 1: Resolve the caller → triggers acquireManagedRequestAuth
authReq, _ := resolver.Determine(req)

// Step 2: Inside acquireManagedRequestAuth:
//    a) pool.AcquireWait(...) gives us `acc` (rotated via bumpQueue)
//    b) ensureManagedToken checks token freshness
//    c) If needed, loginAndPersist refreshes the token and updates tokenRefreshedAt

```

Every request therefore experiences **fair account selection** and **automatic token renewal** without callers managing credentials manually. The refreshed token is cached in `RequestAuth.DeepSeekToken` for the duration of the request.

## Summary

- **Round-robin rotation** is achieved via `bumpQueue` in [`internal/account/pool_acquire.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/account/pool_acquire.go), which moves used account IDs to the end of the queue after each acquisition.
- **Pool initialization** in [`internal/account/pool_core.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/account/pool_core.go) sorts accounts with existing tokens first and configures concurrency limits during `Reset()`.
- **Token refresh** uses a configurable interval (`runtime.token_refresh_interval_hours`) checked by `shouldForceRefresh` in [`internal/auth/request.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/auth/request.go).
- **Automatic renewal** occurs through `ensureManagedToken` and `loginAndPersist`, which handle both empty tokens and expired intervals while updating the persistent store.
- **Thread safety** is maintained throughout via mutex protection in both the account pool and authentication resolver.

## Frequently Asked Questions

### How does DS2API prevent the same account from being used concurrently by multiple requests?

The pool uses a mutex-protected `inUse` map that tracks active request counts per account ID. When `Acquire` selects an account, it increments `inUse[id]` before releasing the lock, and `Release` decrements the count when the request completes. This mechanism ensures that accounts adhere to their configured concurrency limits while remaining available for round-robin rotation.

### What triggers a token refresh in DS2API?

A token refresh triggers under two conditions in [`internal/auth/request.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/auth/request.go): when the account's token string is empty (initial state), or when `shouldForceRefresh` detects that the elapsed time since the last refresh (stored in `tokenRefreshedAt`) exceeds the `token_refresh_interval_hours` configuration. Explicit invalidation via `MarkTokenInvalid` also forces a refresh on the next access.

### How does the round-robin rotation work in the account pool?

The rotation works through the `bumpQueue` method in [`internal/account/pool_acquire.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/account/pool_acquire.go). After an account is acquired, its identifier moves to the end of the queue slice. Subsequent acquisition requests scan the queue from the beginning, naturally receiving the next available account in sequence. This creates a circular distribution of load across all managed accounts.

### Where is the token refresh interval configured?

The refresh interval is defined in [`internal/config/config.go`](https://github.com/CJackHwang/ds2api/blob/main/internal/config/config.go) as `TokenRefreshIntervalHours` within the `Runtime` configuration struct. The default value is 6 hours, and the system reads this value during resolver initialization to determine when cached tokens have expired and require renewal.