Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(regions): move project branches and commits #3843

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
8 changes: 8 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -598,10 +598,18 @@ jobs:
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
command: -c 'max_connections=1000' -c 'port=5433' -c 'wal_level=logical'
- image: 'speckle/speckle-postgres'
environment:
POSTGRES_DB: speckle2_test
POSTGRES_PASSWORD: speckle
POSTGRES_USER: speckle
command: -c 'max_connections=1000' -c 'port=5434' -c 'wal_level=logical'
- image: 'minio/minio'
command: server /data --console-address ":9001" --address "0.0.0.0:9000"
- image: 'minio/minio'
command: server /data --console-address ":9021" --address "0.0.0.0:9020"
- image: 'minio/minio'
command: server /data --console-address ":9041" --address "0.0.0.0:9040"
environment:
# Same as test-server:
NODE_ENV: test
Expand Down
13 changes: 13 additions & 0 deletions .circleci/multiregion.test-ci.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@
"endpoint": "http://127.0.0.1:9020",
"s3Region": "us-east-1"
}
},
"region2": {
"postgres": {
"connectionUri": "postgresql://speckle:speckle@127.0.0.1:5434/speckle2_test"
},
"blobStorage": {
"accessKey": "minioadmin",
"secretKey": "minioadmin",
"bucket": "speckle-server",
"createBucketIfNotExists": true,
"endpoint": "http://127.0.0.1:9040",
"s3Region": "us-east-1"
}
}
}
}
28 changes: 28 additions & 0 deletions docker-compose-deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ services:
ports:
- '127.0.0.1:5401:5432'

postgres-region2:
build:
context: .
dockerfile: utils/postgres/Dockerfile
restart: always
environment:
POSTGRES_DB: speckle
POSTGRES_USER: speckle
POSTGRES_PASSWORD: speckle
volumes:
- postgres-region2-data:/var/lib/postgresql/data/
- ./setup/db/10-docker_postgres_init.sql:/docker-entrypoint-initdb.d/10-docker_postgres_init.sql
- ./setup/db/11-docker_postgres_keycloack_init.sql:/docker-entrypoint-initdb.d/11-docker_postgres_keycloack_init.sql
ports:
- '127.0.0.1:5402:5432'

redis:
image: 'redis:7-alpine'
restart: always
Expand Down Expand Up @@ -62,6 +78,16 @@ services:
- '127.0.0.1:9020:9000'
- '127.0.0.1:9021:9001'

minio-region2:
image: 'minio/minio'
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-region2-data:/data
ports:
- '127.0.0.1:9040:9000'
- '127.0.0.1:9041:9001'

# Local OIDC provider for testing
keycloak:
image: quay.io/keycloak/keycloak:25.0
Expand Down Expand Up @@ -133,8 +159,10 @@ services:
volumes:
postgres-data:
postgres-region1-data:
postgres-region2-data:
redis-data:
pgadmin-data:
redis_insight-data:
minio-data:
minio-region1-data:
minio-region2-data:
12 changes: 12 additions & 0 deletions packages/frontend-2/lib/common/generated/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4580,6 +4580,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
Expand All @@ -4590,6 +4595,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};


export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};


export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
Expand Down Expand Up @@ -8253,6 +8264,7 @@ export type WorkspacePlanFieldArgs = {
}
export type WorkspaceProjectMutationsFieldArgs = {
create: WorkspaceProjectMutationsCreateArgs,
moveToRegion: WorkspaceProjectMutationsMoveToRegionArgs,
moveToWorkspace: WorkspaceProjectMutationsMoveToWorkspaceArgs,
updateRole: WorkspaceProjectMutationsUpdateRoleArgs,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ extend type WorkspaceMutations {
"""
setDefaultRegion(workspaceId: String!, regionKey: String!): Workspace!
}

extend type WorkspaceProjectMutations {
"""
Update project region and move all regional data to new db.
TODO: Currently performs all operations synchronously in request, should probably be scheduled.
"""
moveToRegion(projectId: String!, regionKey: String!): Project!
}
12 changes: 12 additions & 0 deletions packages/server/modules/core/graph/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4603,6 +4603,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
Expand All @@ -4613,6 +4618,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};


export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};


export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
Expand Down Expand Up @@ -6937,6 +6948,7 @@ export type WorkspacePlanResolvers<ContextType = GraphQLContext, ParentType exte

export type WorkspaceProjectMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['WorkspaceProjectMutations'] = ResolversParentTypes['WorkspaceProjectMutations']> = {
create?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsCreateArgs, 'input'>>;
moveToRegion?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToRegionArgs, 'projectId' | 'regionKey'>>;
moveToWorkspace?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsMoveToWorkspaceArgs, 'projectId' | 'workspaceId'>>;
updateRole?: Resolver<ResolversTypes['Project'], ParentType, ContextType, RequireFields<WorkspaceProjectMutationsUpdateRoleArgs, 'input'>>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
Expand Down
2 changes: 1 addition & 1 deletion packages/server/modules/core/services/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const createNewProjectFactory =
async () => {
await getProject({ projectId })
},
{ maxAttempts: 10 }
{ maxAttempts: 100 }
)
} catch (err) {
if (err instanceof StreamNotFoundError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4584,6 +4584,11 @@ export type WorkspaceProjectInviteCreateInput = {
export type WorkspaceProjectMutations = {
__typename?: 'WorkspaceProjectMutations';
create: Project;
/**
* Update project region and move all regional data to new db.
* TODO: Currently performs all operations synchronously in request, should probably be scheduled.
*/
moveToRegion: Project;
moveToWorkspace: Project;
updateRole: Project;
};
Expand All @@ -4594,6 +4599,12 @@ export type WorkspaceProjectMutationsCreateArgs = {
};


export type WorkspaceProjectMutationsMoveToRegionArgs = {
projectId: Scalars['String']['input'];
regionKey: Scalars['String']['input'];
};


export type WorkspaceProjectMutationsMoveToWorkspaceArgs = {
projectId: Scalars['String']['input'];
workspaceId: Scalars['String']['input'];
Expand Down
13 changes: 11 additions & 2 deletions packages/server/modules/multiregion/utils/dbSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ const setUpUserReplication = async ({
try {
await from.public.raw(`CREATE PUBLICATION ${pubName} FOR TABLE users;`)
} catch (err) {
if (!(err instanceof Error))
if (!(err instanceof Error)) {
throw new DatabaseError(
'Could not create publication {pubName} when setting up user replication for region {regionName}',
from.public,
Expand All @@ -215,7 +215,16 @@ const setUpUserReplication = async ({
info: { pubName, regionName }
}
)
if (!err.message.includes('already exists')) throw err
}

const errorMessage = err.message

if (
!['already exists', 'violates unique constraint'].some((message) =>
errorMessage.includes(message)
)
)
throw err
}

const fromUrl = new URL(
Expand Down
4 changes: 2 additions & 2 deletions packages/server/modules/shared/helpers/dbHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function* executeBatchedSelect<
>(
selectQuery: Knex.QueryBuilder<TRecord, TResult>,
options?: Partial<BatchedSelectOptions>
): AsyncGenerator<TResult, void, unknown> {
): AsyncGenerator<Awaited<typeof selectQuery>, void, unknown> {
const { batchSize = 100, trx } = options || {}

if (trx) selectQuery.transacting(trx)
Expand All @@ -33,7 +33,7 @@ export async function* executeBatchedSelect<
let currentOffset = 0
while (hasMorePages) {
const q = selectQuery.clone().offset(currentOffset)
const results = (await q) as TResult
const results = (await q) as Awaited<typeof selectQuery>

if (!results.length) {
hasMorePages = false
Expand Down
23 changes: 22 additions & 1 deletion packages/server/modules/workspaces/domain/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ export type GetAvailableRegions = (params: {
workspaceId: string
}) => Promise<ServerRegion[]>

export type AssignRegion = (params: {
export type AssignWorkspaceRegion = (params: {
workspaceId: string
regionKey: string
}) => Promise<void>
Expand Down Expand Up @@ -348,3 +348,24 @@ export type ApproveWorkspaceJoinRequest = (
export type DenyWorkspaceJoinRequest = (
params: Pick<WorkspaceJoinRequest, 'workspaceId' | 'userId'>
) => Promise<boolean>

/**
* Project regions
*/

/**
* Updates project region and moves all regional data to target regional db
*/
export type UpdateProjectRegion = (params: {
projectId: string
regionKey: string
}) => Promise<Stream>

export type CopyWorkspace = (params: { workspaceId: string }) => Promise<string>
export type CopyProjects = (params: { projectIds: string[] }) => Promise<string[]>
export type CopyProjectModels = (params: {
projectIds: string[]
}) => Promise<Record<string, string[]>>
export type CopyProjectVersions = (params: {
projectIds: string[]
}) => Promise<Record<string, string[]>>
6 changes: 6 additions & 0 deletions packages/server/modules/workspaces/errors/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,9 @@ export class WorkspaceRegionAssignmentError extends BaseError {
static code = 'WORKSPACE_REGION_ASSIGNMENT_ERROR'
static statusCode = 400
}

export class ProjectRegionAssignmentError extends BaseError {
static defaultMessage = 'Failed to assign region to project'
static code = 'PROJECT_REGION_ASSIGNMENT_ERROR'
static statusCode = 400
}
47 changes: 43 additions & 4 deletions packages/server/modules/workspaces/graph/resolvers/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { db } from '@/db/knex'
import { Resolvers } from '@/modules/core/graph/generated/graphql'
import { getWorkspacePlanFactory } from '@/modules/gatekeeper/repositories/billing'
import { canWorkspaceUseRegionsFactory } from '@/modules/gatekeeper/services/featureAuthorization'
import { getDb } from '@/modules/multiregion/utils/dbSelector'
import { getDb, getProjectDbClient } from '@/modules/multiregion/utils/dbSelector'
import { getRegionsFactory } from '@/modules/multiregion/repositories'
import { authorizeResolver } from '@/modules/shared'
import {
copyProjectModelsFactory,
copyProjectsFactory,
copyProjectVersionsFactory,
copyWorkspaceFactory,
getDefaultRegionFactory,
upsertRegionAssignmentFactory
} from '@/modules/workspaces/repositories/regions'
Expand All @@ -14,10 +18,14 @@ import {
upsertWorkspaceFactory
} from '@/modules/workspaces/repositories/workspaces'
import {
assignRegionFactory,
getAvailableRegionsFactory
assignWorkspaceRegionFactory,
getAvailableRegionsFactory,
updateProjectRegionFactory
} from '@/modules/workspaces/services/regions'
import { Roles } from '@speckle/shared'
import { getProjectFactory } from '@/modules/core/repositories/streams'
import { getStreamBranchCountFactory } from '@/modules/core/repositories/branches'
import { getStreamCommitCountFactory } from '@/modules/core/repositories/commits'

export default {
Workspace: {
Expand All @@ -37,7 +45,7 @@ export default {

const regionDb = await getDb({ regionKey: args.regionKey })

const assignRegion = assignRegionFactory({
const assignRegion = assignWorkspaceRegionFactory({
getAvailableRegions: getAvailableRegionsFactory({
getRegions: getRegionsFactory({ db }),
canWorkspaceUseRegions: canWorkspaceUseRegionsFactory({
Expand All @@ -53,5 +61,36 @@ export default {

return await ctx.loaders.workspaces!.getWorkspace.load(args.workspaceId)
}
},
WorkspaceProjectMutations: {
moveToRegion: async (_parent, args, context) => {
await authorizeResolver(
context.userId,
args.projectId,
Roles.Stream.Owner,
context.resourceAccessRules
)

const sourceDb = await getProjectDbClient({ projectId: args.projectId })
const targetDb = await getDb({ regionKey: args.regionKey })

const updateProjectRegion = updateProjectRegionFactory({
getProject: getProjectFactory({ db: sourceDb }),
countProjectModels: getStreamBranchCountFactory({ db: sourceDb }),
countProjectVersions: getStreamCommitCountFactory({ db: sourceDb }),
getAvailableRegions: getAvailableRegionsFactory({
getRegions: getRegionsFactory({ db }),
canWorkspaceUseRegions: canWorkspaceUseRegionsFactory({
getWorkspacePlan: getWorkspacePlanFactory({ db })
})
}),
copyWorkspace: copyWorkspaceFactory({ sourceDb, targetDb }),
copyProjects: copyProjectsFactory({ sourceDb, targetDb }),
copyProjectModels: copyProjectModelsFactory({ sourceDb, targetDb }),
copyProjectVersions: copyProjectVersionsFactory({ sourceDb, targetDb })
})

return await updateProjectRegion(args)
}
}
} as Resolvers
Loading