Files
Daniel Gak Anagrov 75aa5d9b2d feat(ci): reinstate multiregion tests (#5365)
* feat(multiregion): replace user replication

* chore(multiregion): optimise replication

* maybe it's this

* postgres is fun

* once more

* chore(multiregion): only replicate test user creation during multiregion tests

* feat: improved replicate_query logic

* fix: minor

* fix: starting issue

* feat: included user create and delete specs to multiregion

* feat: removed console logs

* fix: user defaults

* fix: multiregion test helper

* fix: update scenarios for users

* refactor(multiregion): swap replicateQuery concept to asMultiregionOperation (#5301)

feat(multiregion): introduced asMultregionOperator, refactor test to user builder classes

* chore: renamings

* fix: remove comments

* feat: remove user replication

* refactor: simplified spec usages

* chore: comments

* chore: branches and favs

* chore: more tests

* chore: more tests

* fix linting

* fix tests

* feat: dropping replication

* refactor: moved project delete to service

* fix: comment

* feat: updateStreamFactory and updateProjectFacotry

* deleteProjectFactory + replicateFactory

* deleteWorkspaceFactory

* fix: selector

* fix: tests

* fix tests, finished createStreamFactory

* feat: simplify changes

* fix: remove comment

* fix: minor strucutres

* fix: moveProjectToRegion

* fix: moved branch creation outside of multiregion scope

* fix: branch creation

* fix: tests

* fix: ci tests

* fix: removed log form test

* fix: on specs, no random regionKeys

* feat: simplify ci for postgres

* try: fix health check

* feat: fixed tests in ci

* try: entrypoint

* try: entrypoint

* try: entrypoint

* try: POSTGRES_INITDB_ARGS

* feat: apply POSTGRES_INITDB_ARGS to all server tests

* fix: broken test

* fix: reinstate max health attempts

* fix: after merge

* fix: after merge

---------

Co-authored-by: Charles Driesler <chuck@speckle.systems>
2025-09-04 14:49:02 +02:00

188 lines
5.8 KiB
TypeScript

import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { Scopes } from '@/modules/core/dbSchema'
import { expect } from 'chai'
import type { Knex } from 'knex'
import { isMultiRegionTestMode } from '@/test/speckle-helpers/regions'
import { db } from '@/db/knex'
import { sleep } from '@/test/helpers'
import { asMultiregionalOperation } from '@/modules/shared/command'
import { logger } from '@/observability/logging'
isMultiRegionTestMode()
? describe('Prepared transaction utils (2PC) @multiregion', async () => {
let main: Knex
let region1: Knex
let region2: Knex
let ALL_DBS: [Knex, ...Knex[]] = [db]
const testOperationFactory =
({ db }: { db: Knex }) =>
async (payload: {
name: string
description: string
public: boolean
}): Promise<void> => {
await db(Scopes.name).insert(payload)
}
before(async () => {
main = db
region1 = await getDb({ regionKey: 'region1' })
region2 = await getDb({ regionKey: 'region2' })
ALL_DBS = [main, region1, region2]
})
beforeEach(async () => {
await db('users').del()
})
it('successfully replicates operation across all specified db instances', async () => {
const testOperationParams = {
name: 'test:scope:a',
description: 'for test purposes only',
public: false
}
await asMultiregionalOperation(
({ allDbs }) =>
Promise.all(
allDbs.map((db) => testOperationFactory({ db })(testOperationParams))
),
{
dbs: ALL_DBS,
name: 'testing regional success',
logger
}
)
const scopeMain = await main
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
const scopeRegion1 = await region1
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
const scopeRegion2 = await region2
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
expect(scopeMain).to.deep.eq(testOperationParams)
expect(scopeMain).to.deep.equal(scopeRegion1)
expect(scopeMain).to.deep.equal(scopeRegion2)
})
it('rolls back when one node fails on write', async () => {
// Create scope before replicated query
const testOperationParams = {
name: 'test:scope:b',
description: 'for test purposes only',
public: false
}
await testOperationFactory({ db: region2 })(testOperationParams)
const promise = asMultiregionalOperation(
({ allDbs }) =>
Promise.all(
allDbs.map((db) => testOperationFactory({ db })(testOperationParams))
),
{
dbs: ALL_DBS,
name: 'testing regional failure',
logger
}
)
await expect(promise).eventually.to.be.rejected
const scopeMain = await main
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
const scopeRegion1 = await region1
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
const scopeRegion2 = await region2
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
expect(scopeMain).to.be.undefined
expect(scopeRegion1).to.be.undefined
expect(scopeRegion2).to.exist
})
it('rolls back all commits in case of one node failure on transaction', async () => {
const testOperationParams = {
name: 'test:scope:c',
description: 'for test purposes only',
public: false
}
const dbThatFails = {
transaction: async () => Promise.reject(new Error('Transaction failed'))
} as unknown as Knex
const promise = asMultiregionalOperation(
({ allDbs }) =>
Promise.all(
allDbs.map((db) => testOperationFactory({ db })(testOperationParams))
),
{
dbs: [...ALL_DBS, dbThatFails],
name: 'testing regional success',
logger
}
)
await expect(promise).to.eventually.be.rejected
const scopeMain = await main
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
const scopeRegion1 = await region1
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
const scopeRegion2 = await region2
.table(Scopes.name)
.where({ name: testOperationParams.name })
.first()
expect(scopeMain).to.be.undefined
expect(scopeRegion1).to.be.undefined
expect(scopeRegion2).to.be.undefined
})
it('does not has visibile perfomance issues using 2PC', async () => {
const connectionsUsedBefore = main.client.pool.numUsed()
const oneKnexInstanceCall = async () => {
const { buildBasicTestUser, createTestUser } = await import(
'@/test/authHelper'
)
const user = buildBasicTestUser()
await createTestUser(user) // This uses the asMultireagionOperation helper }
}
const manyParallelCreates = async () => {
await Promise.allSettled(Array.from({ length: 500 }, oneKnexInstanceCall))
}
await manyParallelCreates()
const [{ count }] = await db('users').count()
expect(count).to.eql('500')
await sleep(50)
const connectionsUsedAfter = main.client.pool.numUsed()
expect(connectionsUsedAfter).to.be.lte(connectionsUsedBefore)
})
})
: null