Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hip-teachers-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@totalsoft/correlation": patch
"@totalsoft/multitenancy-core": patch
---

Fix: Ensure correlation ID and tenant context are kept separate between concurrent calls.
22 changes: 22 additions & 0 deletions packages/correlation/__tests__/correlation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,26 @@ describe('correlation tests:', () => {
//assert
expect(correlationId).toHaveLength(36)
})

it('does not mix correlation ids between concurrent requests', async () => {
//arrange
let correlationId1: string | undefined
let correlationId2: string | undefined

//act
await Promise.all([
correlationManager.useCorrelationId('request-1', async () => {
await new Promise(resolve => setTimeout(resolve, 10))
correlationId1 = correlationManager.getCorrelationId()
}),
correlationManager.useCorrelationId('request-2', async () => {
await new Promise(resolve => setTimeout(resolve, 5))
correlationId2 = correlationManager.getCorrelationId()
})
])

//assert
expect(correlationId1).toBe('request-1')
expect(correlationId2).toBe('request-2')
})
})
11 changes: 4 additions & 7 deletions packages/correlation/src/correlationManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
import { AsyncLocalStorage } from 'async_hooks'
import { v4 } from 'uuid'

const asyncLocalStorage = new AsyncLocalStorage<Map<string, string>>()
const store = new Map<string, string>()
const correlationIdKey = 'correlationId'
const asyncLocalStorage = new AsyncLocalStorage<string>()

const getCorrelationId = () => {
const correlationIdStore = asyncLocalStorage.getStore()
return correlationIdStore?.get(correlationIdKey)
return asyncLocalStorage.getStore()
}

async function useCorrelationId(correlationId: string | null, next: () => Promise<void>) {
return asyncLocalStorage.run(store, async () => {
store.set(correlationIdKey, correlationId || v4())
const correlationIdToUse = correlationId || v4()
return asyncLocalStorage.run(correlationIdToUse, async () => {
return await next()
})
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright (c) TotalSoft.
// This source code is licensed under the MIT license.

import { tenantContextAccessor } from '../src'

describe('tenant context accessor tests:', () => {
it('passes tenant context in async flow', async () => {
//arrange
const tenant = { id: 'tenant1', code: 'tenant1-code', enabled: true }
let tenantContext
async function inner() {
tenantContext = tenantContextAccessor.getTenantContext()
}

//act
await tenantContextAccessor.useTenantContext({ tenant }, async () => {
await inner()
})

//assert
expect(tenantContext).toHaveProperty('tenant', tenant)
})

it('returns empty context if tenant context not set', async () => {
//arrange
let tenantContext
async function inner() {
tenantContext = tenantContextAccessor.getTenantContext()
}

//act
await inner()

//assert
expect(tenantContext).not.toBe(undefined)
expect(tenantContext).not.toHaveProperty('tenant')
})

it('does not share tenant context between concurrent requests', async () => {
//arrange
const tenant1 = { id: 'tenant1', code: 'tenant1-code', enabled: true }
const tenant2 = { id: 'tenant2', code: 'tenant2-code', enabled: true }
let capturedContext1: ReturnType<typeof tenantContextAccessor.getTenantContext> = {} as ReturnType<typeof tenantContextAccessor.getTenantContext>
let capturedContext2: ReturnType<typeof tenantContextAccessor.getTenantContext> = {} as ReturnType<typeof tenantContextAccessor.getTenantContext>

const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))

async function request1() {
await tenantContextAccessor.useTenantContext({ tenant: tenant1 }, async () => {
await delay(10)
capturedContext1 = tenantContextAccessor.getTenantContext()
await delay(10)
})
}

async function request2() {
await tenantContextAccessor.useTenantContext({ tenant: tenant2 }, async () => {
await delay(5)
capturedContext2 = tenantContextAccessor.getTenantContext()
await delay(15)
})
}

//act
await Promise.all([request1(), request2()])

//assert
expect(capturedContext1).toHaveProperty('tenant', tenant1)
expect(capturedContext2).toHaveProperty('tenant', tenant2)
})
})

This file was deleted.

10 changes: 4 additions & 6 deletions packages/multitenancy-core/src/tenantContextAccessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,15 @@
import { AsyncLocalStorage } from 'async_hooks'
import { TenantContext } from './types'

const asyncLocalStorage = new AsyncLocalStorage<Map<string, TenantContext>>()
const store = new Map<string, TenantContext>()
const asyncLocalStorage = new AsyncLocalStorage<TenantContext>()

/**
* Access the current tenant context in scope
* @returns - the tenant context
*/
const getTenantContext = (): TenantContext => {
const tenantStore = asyncLocalStorage.getStore()
return tenantStore?.get('tenantContext') ?? <TenantContext>{}
const tenantContext = asyncLocalStorage.getStore()
return tenantContext ?? <TenantContext>{}
}
Comment thread
dragos-rosca marked this conversation as resolved.

/**
Expand All @@ -23,8 +22,7 @@ const getTenantContext = (): TenantContext => {
* @returns the result of the next function
*/
async function useTenantContext(tenantContext: TenantContext, next: () => Promise<void>) {
return asyncLocalStorage.run(store, async () => {
store.set('tenantContext', tenantContext)
return asyncLocalStorage.run(tenantContext, async () => {
return await next()
})
}
Expand Down
2 changes: 1 addition & 1 deletion packages/multitenancy-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ export interface TenantSection {
}

export interface TenantContext {
tenant: Tenant;
tenant?: Tenant
}