Skip to content

Commit

Permalink
SUPPORT-27819 fix concurrent lock acquiring when concurrently fetchin…
Browse files Browse the repository at this point in the history
…g token (#777)

* SUPPORT-27819 fix concurrent lock acquiring when concurrently fetching token

* chore(tokenCache): re-introduce token cache

- re-introduce store as a default value for tokenCache to fix tokenCache.get error.

* chore(auth-request-executor): skip test

- skip test as implementation was replaced

* chore(SUPPORT-27819): remove unnecessary comments

- remove unnecessary and unused code blocks

* chore(release-changeset): add release changeset

- add release changeset for @commercetools/ts-client

---------

Co-authored-by: Chukwuemeka Ajima <[email protected]>
Co-authored-by: Ajima Chukwuemeka <[email protected]>
  • Loading branch information
3 people authored Aug 9, 2024
1 parent e76b5cf commit 38c7035
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-eels-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@commercetools/ts-client': patch
---

Bug fixes for @commercetools/ts-client
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { apiRoot } from '../test-utils'
import { CategoryDraft } from '../../../src'
import { randomUUID } from 'crypto'

const categoryDraft: CategoryDraft = {
key: 'test-key-category-' + randomUUID(),
name: { en: 'test-name-category' + randomUUID() },
slug: { en: 'test-slug-category' + randomUUID() },
}

export const createCategory = async () => {
const categoryDraft: CategoryDraft = {
key: 'test-key-category-' + randomUUID(),
name: { en: 'test-name-category' + randomUUID() },
slug: { en: 'test-slug-category' + randomUUID() },
}
return await apiRoot.categories().post({ body: categoryDraft }).execute()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { createCategory } from '../category/category-fixture'
import {
authMiddlewareOptionsV3,
createTokenCache,
httpMiddlewareOptionsV3,
projectKey,
} from '../test-utils'
import { ClientBuilder as ClientBuilderV3 } from '@commercetools/ts-client/src'
import { createApiBuilderFromCtpClient } from '../../../src'

describe('Concurrent processing in sdk v3', () => {
it(`should fetch 2 categories concurrently`, async () => {
const category1 = await createCategory()
const category2 = await createCategory()

const tokenCache = createTokenCache()

authMiddlewareOptionsV3.tokenCache = tokenCache

const client = new ClientBuilderV3()
.withClientCredentialsFlow(authMiddlewareOptionsV3)
.withHttpMiddleware(httpMiddlewareOptionsV3)
.withConcurrentModificationMiddleware()
.build()

const apiRoot = createApiBuilderFromCtpClient(client).withProjectKey({
projectKey,
})

const key = category1.body.key
const key2 = category2.body.key

const [response1, response2] = await Promise.all([
apiRoot.categories().withKey({ key }).get().execute(),
apiRoot.categories().withKey({ key: key2 }).get().execute(),
])

expect(response1.statusCode).toBe(200)
expect(response2.statusCode).toBe(200)
})
})
7 changes: 7 additions & 0 deletions packages/platform-sdk/test/integration-tests/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ export const authMiddlewareOptionsV3 = {
httpClient: fetch,
}

export function createTokenCache() {
return _tokenCache<TokenStore, TokenCache>({
token: '',
expirationTime: -1,
})
}

const ctpClient = new ClientBuilder()
.withProjectKey(projectKey)
.withClientCredentialsFlow(authMiddlewareOptions)
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-client-v3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"abort-controller": "3.0.0",
"async-mutex": "^0.5.0",
"buffer": "^6.0.3",
"node-fetch": "^2.6.1",
"uuid": "10.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,8 @@ import { buildRequestForRefreshTokenFlow } from './auth-request-builder'
export async function executeRequest(
options: executeRequestOptions
): Promise<ClientRequest> {
const {
request,
httpClient,
tokenCache,
tokenCacheKey,
requestState,
userOption,
next,
} = options
const { request, httpClient, tokenCache, tokenCacheKey, userOption, next } =
options

let url = options.url
let body = options.body
Expand Down Expand Up @@ -59,12 +52,6 @@ export async function executeRequest(
*/
pendingTasks.push({ request, next })

// if a token is currently being fetched, then wait
if (requestState.get()) return

// signal that a token is being fetched
requestState.set(true)

/**
* use refreshToken flow if there is refresh-token
* and there's either no token or the token is expired
Expand All @@ -76,7 +63,6 @@ export async function executeRequest(
(tokenCacheObject.token && Date.now() > tokenCacheObject.expirationTime))
) {
if (!userOption) {
requestState.set(false)
throw new Error('Missing required options.')
}

Expand Down Expand Up @@ -121,9 +107,6 @@ export async function executeRequest(
// cache new generated token, refreshToken and expiration time
tokenCache.set({ token, expirationTime, refreshToken })

// signal that a token fetch is complete
requestState.set(false)

/**
* Freeze and copy pending queue, reset
* original one for accepting new pending tasks
Expand Down Expand Up @@ -156,10 +139,8 @@ export async function executeRequest(
)
/**
* reject the error immediately
* and free up the middleware chain and
* release the requestState by setting it to false
* and free up the middleware chain
*/
requestState.set(false)
request.reject({
...request,
headers: { ...request.headers },
Expand All @@ -169,10 +150,6 @@ export async function executeRequest(
},
})
} catch (error) {
/**
* on error release the state by setting it to false
*/
requestState.set(false)
return {
...request,
headers: { ...request.headers },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fetch from 'node-fetch'
import { Mutex } from 'async-mutex'
import {
AuthMiddlewareOptions,
Middleware,
Expand All @@ -18,7 +19,7 @@ import { executeRequest } from './auth-request-executor'
export default function createAuthMiddlewareForClientCredentialsFlow(
options: AuthMiddlewareOptions
): Middleware {
const requestState = store<RequestState, RequestStateStore>(false)
const requestState = new Mutex()
const pendingTasks: Array<Task> = []
const tokenCache =
options.tokenCache ||
Expand Down Expand Up @@ -52,7 +53,13 @@ export default function createAuthMiddlewareForClientCredentialsFlow(
}

// make request to coco
const requestWithAuth = await executeRequest(requestOptions)
let requestWithAuth
try {
await requestState.acquire()
requestWithAuth = await executeRequest(requestOptions)
} finally {
requestState.release()
}
if (requestWithAuth) {
// make the request and inject the token into the header
return next(requestWithAuth)
Expand Down
5 changes: 1 addition & 4 deletions packages/sdk-client-v3/src/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,7 @@ export type RefreshAuthMiddlewareOptions = {
httpClient?: Function
}

export type RequestStateStore = {
get: () => RequestState
set: (requestState: RequestState) => void
}
export type RequestStateStore = any

/* Request */
type requestBaseOptions = {
Expand Down
19 changes: 0 additions & 19 deletions packages/sdk-client-v3/tests/auth/auth-request-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,6 @@ describe('Auth request executor', () => {
)
})

test('should return if a token is already being fetched', async () => {
const options = createTestExecutorOptions({
requestState: { get: jest.fn(() => true), set: jest.fn() },
tokenCache: {
set: jest.fn(),
get: jest.fn(() => ({
token: 'test-cached-token',
expirationTime: Date.now() - 999,
})),
},
httpClient: jest.fn(() => ({
statusCode: 200,
data: { statusCode: 200, access_token: 'test-access-token' },
})),
})

expect(await executeRequest(options)).toEqual(undefined)
})

test('should throw if userOptions are not provided for token refresh', () => {
const options = createTestExecutorOptions({
userOption: null,
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3922,6 +3922,13 @@ assert@^2.1.0:
object.assign "^4.1.4"
util "^0.12.5"

async-mutex@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/async-mutex/-/async-mutex-0.5.0.tgz#353c69a0b9e75250971a64ac203b0ebfddd75482"
integrity sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==
dependencies:
tslib "^2.4.0"

at-least-node@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz"
Expand Down Expand Up @@ -8766,6 +8773,11 @@ tsconfig@^7.0.0:
strip-bom "^3.0.0"
strip-json-comments "^2.0.0"

tslib@^2.4.0:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==

tty-browserify@^0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz"
Expand Down

0 comments on commit 38c7035

Please sign in to comment.