UBERF-9633: Reduce migration calls during workspace creation (#8242) (#8244)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-03-17 17:44:24 +07:00 committed by GitHub
parent 32550b6f9d
commit 419a3ddfdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 260 additions and 144 deletions

View File

@ -38,6 +38,7 @@ import {
type MigrationDocumentQuery, type MigrationDocumentQuery,
type MigrationIterator, type MigrationIterator,
type MigrationUpgradeClient, type MigrationUpgradeClient,
type MigrateMode,
tryMigrate tryMigrate
} from '@hcengineering/model' } from '@hcengineering/model'
import { htmlToMarkup } from '@hcengineering/text' import { htmlToMarkup } from '@hcengineering/text'
@ -456,18 +457,21 @@ async function migrateSocialIdsInDocUpdates (client: MigrationClient): Promise<v
} }
export const activityOperation: MigrateOperation = { export const activityOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient, mode: MigrateMode): Promise<void> {
await tryMigrate(client, activityId, [ await tryMigrate(client, activityId, [
{ {
state: 'reactions', state: 'reactions',
mode: 'upgrade',
func: migrateReactions func: migrateReactions
}, },
{ {
state: 'markup', state: 'markup',
mode: 'upgrade',
func: migrateMarkup func: migrateMarkup
}, },
{ {
state: 'migrate-doc-update-messages-space', state: 'migrate-doc-update-messages-space',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await migrateMessagesSpace( await migrateMessagesSpace(
client, client,
@ -477,6 +481,30 @@ export const activityOperation: MigrateOperation = {
) )
} }
}, },
{
state: 'migrate-employee-space-v1',
mode: 'upgrade',
func: async () => {
await client.update<ActivityMessage>(
DOMAIN_ACTIVITY,
{ space: 'contact:space:Employee' as Ref<Space> },
{ space: contact.space.Contacts }
)
}
},
{
state: 'migrate-activity-markup',
mode: 'upgrade',
func: migrateActivityMarkup
},
{
state: 'move-reactions',
mode: 'upgrade',
func: async (client: MigrationClient): Promise<void> => {
await client.move(DOMAIN_ACTIVITY, { _class: activity.class.Reaction }, DOMAIN_REACTION)
await client.move(DOMAIN_ACTIVITY, { _class: activity.class.UserMentionInfo }, DOMAIN_USER_MENTION)
}
},
{ {
state: 'migrate-employee-space-v1', state: 'migrate-employee-space-v1',
func: async () => { func: async () => {

View File

@ -236,14 +236,17 @@ export const chunterOperation: MigrateOperation = {
await tryMigrate(client, chunterId, [ await tryMigrate(client, chunterId, [
{ {
state: 'create-chat-messages', state: 'create-chat-messages',
mode: 'upgrade',
func: convertCommentsToChatMessages func: convertCommentsToChatMessages
}, },
{ {
state: 'remove-backlinks', state: 'remove-backlinks',
mode: 'upgrade',
func: removeBacklinks func: removeBacklinks
}, },
{ {
state: 'migrate-chat-messages-space', state: 'migrate-chat-messages-space',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await migrateMessagesSpace( await migrateMessagesSpace(
client, client,
@ -255,6 +258,7 @@ export const chunterOperation: MigrateOperation = {
}, },
{ {
state: 'migrate-thread-messages-space', state: 'migrate-thread-messages-space',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await migrateMessagesSpace( await migrateMessagesSpace(
client, client,
@ -266,18 +270,21 @@ export const chunterOperation: MigrateOperation = {
}, },
{ {
state: 'remove-old-classes-v1', state: 'remove-old-classes-v1',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await removeOldClasses(client) await removeOldClasses(client)
} }
}, },
{ {
state: 'remove-wrong-activity-v1', state: 'remove-wrong-activity-v1',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await removeWrongActivity(client) await removeWrongActivity(client)
} }
}, },
{ {
state: 'remove-chat-info-v1', state: 'remove-chat-info-v1',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await client.deleteMany(DOMAIN_CHUNTER, { _class: 'chunter:class:ChatInfo' as Ref<Class<Doc>> }) await client.deleteMany(DOMAIN_CHUNTER, { _class: 'chunter:class:ChatInfo' as Ref<Class<Doc>> })
await client.deleteMany(DOMAIN_TX, { objectClass: 'chunter:class:ChatInfo' }) await client.deleteMany(DOMAIN_TX, { objectClass: 'chunter:class:ChatInfo' })
@ -291,6 +298,7 @@ export const chunterOperation: MigrateOperation = {
}, },
{ {
state: 'remove-direct-doc-update-messages', state: 'remove-direct-doc-update-messages',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, { await client.deleteMany<DocUpdateMessage>(DOMAIN_ACTIVITY, {
_class: activity.class.DocUpdateMessage, _class: activity.class.DocUpdateMessage,

View File

@ -22,7 +22,6 @@ import {
type MigrationClient, type MigrationClient,
type MigrationDocumentQuery, type MigrationDocumentQuery,
type MigrationUpgradeClient, type MigrationUpgradeClient,
type ModelLogger,
tryMigrate, tryMigrate,
tryUpgrade tryUpgrade
} from '@hcengineering/model' } from '@hcengineering/model'
@ -264,7 +263,7 @@ async function createSocialIdentities (client: MigrationClient): Promise<void> {
} }
export const contactOperation: MigrateOperation = { export const contactOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, contactId, [ await tryMigrate(client, contactId, [
{ {
state: 'employees', state: 'employees',

View File

@ -55,6 +55,7 @@ import {
createDefaultSpace, createDefaultSpace,
tryMigrate, tryMigrate,
tryUpgrade, tryUpgrade,
type MigrateMode,
type MigrateOperation, type MigrateOperation,
type MigrateUpdate, type MigrateUpdate,
type MigrationClient, type MigrationClient,
@ -64,7 +65,7 @@ import {
} from '@hcengineering/model' } from '@hcengineering/model'
import { type StorageAdapter } from '@hcengineering/storage' import { type StorageAdapter } from '@hcengineering/storage'
async function migrateStatusesToModel (client: MigrationClient): Promise<void> { async function migrateStatusesToModel (client: MigrationClient, mode: MigrateMode): Promise<void> {
// Move statuses to model: // Move statuses to model:
// Migrate the default ones with well-known ids as system's model // Migrate the default ones with well-known ids as system's model
// And the rest as user's model // And the rest as user's model
@ -887,26 +888,32 @@ export const coreOperation: MigrateOperation = {
await tryMigrate(client, coreId, [ await tryMigrate(client, coreId, [
{ {
state: 'statuses-to-model', state: 'statuses-to-model',
mode: 'upgrade',
func: migrateStatusesToModel func: migrateStatusesToModel
}, },
{ {
state: 'all-space-to-typed', state: 'all-space-to-typed',
mode: 'upgrade',
func: migrateAllSpaceToTyped func: migrateAllSpaceToTyped
}, },
{ {
state: 'add-spaces-owner-v1', state: 'add-spaces-owner-v1',
mode: 'upgrade',
func: migrateSpacesOwner func: migrateSpacesOwner
}, },
{ {
state: 'old-statuses-transactions', state: 'old-statuses-transactions',
mode: 'upgrade',
func: migrateStatusTransactions func: migrateStatusTransactions
}, },
{ {
state: 'collaborative-content-to-storage', state: 'collaborative-content-to-storage',
mode: 'upgrade',
func: migrateCollaborativeContentToStorage func: migrateCollaborativeContentToStorage
}, },
{ {
state: 'fix-backups-hash-timestamp-v2', state: 'fix-backups-hash-timestamp-v2',
mode: 'upgrade',
func: async (client: MigrationClient): Promise<void> => { func: async (client: MigrationClient): Promise<void> => {
const now = Date.now().toString(16) const now = Date.now().toString(16)
for (const d of client.hierarchy.domains()) { for (const d of client.hierarchy.domains()) {
@ -916,6 +923,7 @@ export const coreOperation: MigrateOperation = {
}, },
{ {
state: 'remove-collection-txes', state: 'remove-collection-txes',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
let processed = 0 let processed = 0
let last = 0 let last = 0
@ -959,6 +967,7 @@ export const coreOperation: MigrateOperation = {
}, },
{ {
state: 'move-model-txes', state: 'move-model-txes',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
await client.move( await client.move(
DOMAIN_TX, DOMAIN_TX,
@ -971,15 +980,18 @@ export const coreOperation: MigrateOperation = {
}, },
{ {
state: 'collaborative-docs-to-json', state: 'collaborative-docs-to-json',
mode: 'upgrade',
func: migrateCollaborativeDocsToJson func: migrateCollaborativeDocsToJson
}, },
{ {
state: 'accounts-to-social-ids', state: 'accounts-to-social-ids',
mode: 'upgrade',
func: migrateAccounts func: migrateAccounts
}, },
// ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION // ONLY FOR STAGING. REMOVE IT BEFORE MERGING TO PRODUCTION
{ {
state: 'space-members-to-account-uuids', state: 'space-members-to-account-uuids',
mode: 'upgrade',
func: migrateSpaceMembersToAccountUuids func: migrateSpaceMembersToAccountUuids
} }
]) ])

View File

@ -357,42 +357,52 @@ export const documentOperation: MigrateOperation = {
await tryMigrate(client, documentId, [ await tryMigrate(client, documentId, [
{ {
state: 'updateDocumentIcons', state: 'updateDocumentIcons',
mode: 'upgrade',
func: migrateDocumentIcons func: migrateDocumentIcons
}, },
{ {
state: 'migrate-timespaces', state: 'migrate-timespaces',
mode: 'upgrade',
func: migrateTeamspaces func: migrateTeamspaces
}, },
{ {
state: 'migrate-teamspaces-mixins', state: 'migrate-teamspaces-mixins',
mode: 'upgrade',
func: migrateTeamspacesMixins func: migrateTeamspacesMixins
}, },
{ {
state: 'migrateRank', state: 'migrateRank',
mode: 'upgrade',
func: migrateRank func: migrateRank
}, },
{ {
state: 'renameFields', state: 'renameFields',
mode: 'upgrade',
func: renameFields func: renameFields
}, },
{ {
state: 'renameFieldsRevert', state: 'renameFieldsRevert',
mode: 'upgrade',
func: renameFieldsRevert func: renameFieldsRevert
}, },
{ {
state: 'restoreContentField', state: 'restoreContentField',
mode: 'upgrade',
func: restoreContentField func: restoreContentField
}, },
{ {
state: 'migrateRanks', state: 'migrateRanks',
mode: 'upgrade',
func: migrateRanks func: migrateRanks
}, },
{ {
state: 'removeOldClasses', state: 'removeOldClasses',
mode: 'upgrade',
func: removeOldClasses func: removeOldClasses
}, },
{ {
state: 'migrateEmbeddings', state: 'migrateEmbeddings',
mode: 'upgrade',
func: migrateEmbeddings func: migrateEmbeddings
}, },
{ {
@ -401,6 +411,7 @@ export const documentOperation: MigrateOperation = {
}, },
{ {
state: 'migrateEmbeddingsRefs', state: 'migrateEmbeddingsRefs',
mode: 'upgrade',
func: migrateEmbeddingsRefs func: migrateEmbeddingsRefs
} }
]) ])

View File

@ -3,12 +3,11 @@ import {
tryMigrate, tryMigrate,
type MigrateOperation, type MigrateOperation,
type MigrationClient, type MigrationClient,
type MigrationUpgradeClient, type MigrationUpgradeClient
type ModelLogger
} from '@hcengineering/model' } from '@hcengineering/model'
export const guestOperation: MigrateOperation = { export const guestOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, guestId, []) await tryMigrate(client, guestId, [])
}, },
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {} async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}

View File

@ -17,12 +17,11 @@ import {
tryMigrate, tryMigrate,
type MigrateOperation, type MigrateOperation,
type MigrationClient, type MigrationClient,
type MigrationUpgradeClient, type MigrationUpgradeClient
type ModelLogger
} from '@hcengineering/model' } from '@hcengineering/model'
export const requestOperation: MigrateOperation = { export const requestOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, requestId, []) await tryMigrate(client, requestId, [])
}, },
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {} async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {}

View File

@ -267,6 +267,7 @@ export const activityServerOperation: MigrateOperation = {
await tryMigrate(client, serverActivityId, [ await tryMigrate(client, serverActivityId, [
{ {
state: 'doc-update-messages', state: 'doc-update-messages',
mode: 'upgrade',
func: async (client) => { func: async (client) => {
// Recreate activity to avoid duplicates // Recreate activity to avoid duplicates
await client.deleteMany(DOMAIN_ACTIVITY, { await client.deleteMany(DOMAIN_ACTIVITY, {

View File

@ -18,7 +18,6 @@ import {
type MigrateOperation, type MigrateOperation,
type MigrationClient, type MigrationClient,
type MigrationUpgradeClient, type MigrationUpgradeClient,
type ModelLogger,
tryMigrate, tryMigrate,
tryUpgrade tryUpgrade
} from '@hcengineering/model' } from '@hcengineering/model'
@ -26,7 +25,7 @@ import {
import survey, { surveyId } from './index' import survey, { surveyId } from './index'
export const surveyOperation: MigrateOperation = { export const surveyOperation: MigrateOperation = {
async migrate (client: MigrationClient, logger: ModelLogger): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
await tryMigrate(client, surveyId, []) await tryMigrate(client, surveyId, [])
}, },
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> { async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>): Promise<void> {

View File

@ -122,20 +122,21 @@ export interface MigrationClient {
* @public * @public
*/ */
export type MigrationUpgradeClient = Client export type MigrationUpgradeClient = Client
export type MigrateMode = 'create' | 'upgrade'
/** /**
* @public * @public
*/ */
export interface MigrateOperation { export interface MigrateOperation {
// Perform low level migration prior to the model update // Perform low level migration prior to the model update
preMigrate?: (client: MigrationClient, logger: ModelLogger) => Promise<void> preMigrate?: (client: MigrationClient, logger: ModelLogger, mode: MigrateMode) => Promise<void>
// Perform low level migration // Perform low level migration
migrate: (client: MigrationClient, logger: ModelLogger) => Promise<void> migrate: (client: MigrationClient, mode: MigrateMode) => Promise<void>
// Perform high level upgrade operations. // Perform high level upgrade operations.
upgrade: ( upgrade: (
state: Map<string, Set<string>>, state: Map<string, Set<string>>,
client: () => Promise<MigrationUpgradeClient>, client: () => Promise<MigrationUpgradeClient>,
logger: ModelLogger mode: MigrateMode
) => Promise<void> ) => Promise<void>
} }
@ -144,7 +145,8 @@ export interface MigrateOperation {
*/ */
export interface Migrations { export interface Migrations {
state: string state: string
func: (client: MigrationClient) => Promise<void> mode?: MigrateMode // If set only applied to specified mode
func: (client: MigrationClient, mode: MigrateMode) => Promise<void>
} }
/** /**
@ -152,23 +154,31 @@ export interface Migrations {
*/ */
export interface UpgradeOperations { export interface UpgradeOperations {
state: string state: string
func: (client: MigrationUpgradeClient) => Promise<void> mode?: MigrateMode // If set only applied to specified mode
func: (client: MigrationUpgradeClient, mode: MigrateMode) => Promise<void>
} }
/** /**
* @public * @public
*/ */
export async function tryMigrate (client: MigrationClient, plugin: string, migrations: Migrations[]): Promise<void> { export async function tryMigrate (
client: MigrationClient,
plugin: string,
migrations: Migrations[],
mode: MigrateMode = 'upgrade'
): Promise<void> {
const states = client.migrateState.get(plugin) ?? new Set() const states = client.migrateState.get(plugin) ?? new Set()
for (const migration of migrations) { for (const migration of migrations) {
if (states.has(migration.state)) continue if (states.has(migration.state)) continue
try { if (migration.mode == null || migration.mode === mode) {
console.log('running migration', plugin, migration.state) try {
await migration.func(client) console.log('running migration', plugin, migration.state)
} catch (err: any) { await migration.func(client, mode)
console.error(err) } catch (err: any) {
Analytics.handleError(err) console.error(err)
continue Analytics.handleError(err)
continue
}
} }
const st: MigrationState = { const st: MigrationState = {
plugin, plugin,
@ -190,22 +200,25 @@ export async function tryUpgrade (
state: Map<string, Set<string>>, state: Map<string, Set<string>>,
client: () => Promise<MigrationUpgradeClient>, client: () => Promise<MigrationUpgradeClient>,
plugin: string, plugin: string,
migrations: UpgradeOperations[] migrations: UpgradeOperations[],
mode: MigrateMode = 'upgrade'
): Promise<void> { ): Promise<void> {
const states = state.get(plugin) ?? new Set() const states = state.get(plugin) ?? new Set()
for (const migration of migrations) { for (const upgrades of migrations) {
if (states.has(migration.state)) continue if (states.has(upgrades.state)) continue
const _client = await client() const _client = await client()
try { if (upgrades.mode == null || upgrades.mode === mode) {
await migration.func(_client) try {
} catch (err: any) { await upgrades.func(_client, mode)
console.error(err) } catch (err: any) {
Analytics.handleError(err) console.error(err)
continue Analytics.handleError(err)
continue
}
} }
const st: Data<MigrationState> = { const st: Data<MigrationState> = {
plugin, plugin,
state: migration.state state: upgrades.state
} }
const tx = new TxOperations(_client, core.account.System) const tx = new TxOperations(_client, core.account.System)
await tx.createDoc(core.class.MigrationState, core.space.Configuration, st) await tx.createDoc(core.class.MigrationState, core.space.Configuration, st)

View File

@ -0,0 +1,13 @@
#!/usr/bin/env bash
export INIT_SCRIPTS_BRANCH=${INIT_SCRIPTS_BRANCH:-unified-init-scripts}
# Download init repository
# Check if the file already exists
if [ -e "${INIT_SCRIPTS_BRANCH}.zip" ]; then
echo "File ${INIT_SCRIPTS_BRANCH}.zip already exists, skipping download"
else
wget https://github.com/hcengineering/init/archive/refs/heads/${INIT_SCRIPTS_BRANCH}.zip
fi
unzip ${INIT_SCRIPTS_BRANCH}.zip -d ./temp
mv temp/init-${INIT_SCRIPTS_BRANCH} ./init
rm -rf ./temp

View File

@ -263,7 +263,7 @@ class BackupWorker {
workspace: ws.uuid, workspace: ws.uuid,
url: ws.url url: ws.url
}) })
const ctx = rootCtx.newChild(ws.uuid, { workspace: ws.uuid, url: ws.url }) const ctx = rootCtx.newChild('doBackup', {})
const dataId = ws.dataId ?? (ws.uuid as unknown as WorkspaceDataId) const dataId = ws.dataId ?? (ws.uuid as unknown as WorkspaceDataId)
let pipeline: Pipeline | undefined let pipeline: Pipeline | undefined
const backupIds = { const backupIds = {
@ -278,54 +278,58 @@ class BackupWorker {
dataId: ws.dataId, dataId: ws.dataId,
url: ws.url url: ws.url
} }
const result = await ctx.with('backup', { workspace: ws.uuid, url: ws.url }, (ctx) => const result = await ctx.with(
backup(ctx, '', wsIds, storage, { 'backup',
skipDomains: this.skipDomains, {},
force: true, (ctx) =>
timeout: this.config.Timeout * 1000, backup(ctx, '', wsIds, storage, {
connectTimeout: 5 * 60 * 1000, // 5 minutes to, skipDomains: this.skipDomains,
blobDownloadLimit: this.downloadLimit, force: true,
skipBlobContentTypes: [], timeout: this.config.Timeout * 1000,
fullVerify: this.fullCheck, connectTimeout: 5 * 60 * 1000, // 5 minutes to,
storageAdapter: this.workspaceStorageAdapter, blobDownloadLimit: this.downloadLimit,
getLastTx: async (): Promise<Tx | undefined> => { skipBlobContentTypes: [],
const config = this.getConfig(ctx, wsIds, null, this.workspaceStorageAdapter) fullVerify: this.fullCheck,
const adapterConf = config.adapters[config.domains[DOMAIN_TX]] storageAdapter: this.workspaceStorageAdapter,
const hierarchy = new Hierarchy() getLastTx: async (): Promise<Tx | undefined> => {
const modelDb = new ModelDb(hierarchy) const config = this.getConfig(ctx, wsIds, null, this.workspaceStorageAdapter)
const txAdapter = await adapterConf.factory( const adapterConf = config.adapters[config.domains[DOMAIN_TX]]
ctx, const hierarchy = new Hierarchy()
this.contextVars, const modelDb = new ModelDb(hierarchy)
hierarchy, const txAdapter = await adapterConf.factory(
adapterConf.url, ctx,
wsIds, this.contextVars,
modelDb, hierarchy,
this.workspaceStorageAdapter adapterConf.url,
) wsIds,
try { modelDb,
await txAdapter.init?.(ctx, this.contextVars) this.workspaceStorageAdapter
)
try {
await txAdapter.init?.(ctx, this.contextVars)
return ( return (
await txAdapter.rawFindAll<Tx>( await txAdapter.rawFindAll<Tx>(
DOMAIN_TX, DOMAIN_TX,
{ objectSpace: { $ne: core.space.Model } }, { objectSpace: { $ne: core.space.Model } },
{ limit: 1, sort: { modifiedOn: SortingOrder.Descending } } { limit: 1, sort: { modifiedOn: SortingOrder.Descending } }
) )
).shift() ).shift()
} finally { } finally {
await txAdapter.close() await txAdapter.close()
}
},
getConnection: async () => {
if (pipeline === undefined) {
pipeline = await this.pipelineFactory(ctx, wsIds, true, () => {}, null)
}
return wrapPipeline(ctx, pipeline, wsIds)
},
progress: (progress) => {
return notify?.(progress) ?? Promise.resolve()
} }
}, }),
getConnection: async () => { { workspace: ws.uuid, url: ws.url }
if (pipeline === undefined) {
pipeline = await this.pipelineFactory(ctx, wsIds, true, () => {}, null)
}
return wrapPipeline(ctx, pipeline, wsIds)
},
progress: (progress) => {
return notify?.(progress) ?? Promise.resolve()
}
})
) )
if (result.result) { if (result.result) {
@ -444,28 +448,32 @@ export async function doRestoreWorkspace (
rootCtx.warn('\nRESTORE WORKSPACE ', { rootCtx.warn('\nRESTORE WORKSPACE ', {
workspace: wsIds.uuid workspace: wsIds.uuid
}) })
const ctx = rootCtx.newChild(wsIds.uuid, { workspace: wsIds.uuid }) const ctx = rootCtx.newChild('doRestore', {})
let pipeline: Pipeline | undefined let pipeline: Pipeline | undefined
try { try {
const restoreIds = { uuid: bucketName as WorkspaceUuid, dataId: bucketName as WorkspaceDataId, url: '' } const restoreIds = { uuid: bucketName as WorkspaceUuid, dataId: bucketName as WorkspaceDataId, url: '' }
const storage = await createStorageBackupStorage(ctx, backupAdapter, restoreIds, wsIds.uuid) const storage = await createStorageBackupStorage(ctx, backupAdapter, restoreIds, wsIds.uuid)
const result: boolean = await ctx.with('restore', { workspace: wsIds.uuid }, (ctx) => const result: boolean = await ctx.with(
restore(ctx, '', wsIds, storage, { 'restore',
date: -1, {},
skip: new Set(skipDomains), (ctx) =>
recheck: false, // Do not need to recheck restore(ctx, '', wsIds, storage, {
storageAdapter: workspaceStorageAdapter, date: -1,
cleanIndexState, skip: new Set(skipDomains),
getConnection: async () => { recheck: false, // Do not need to recheck
if (pipeline === undefined) { storageAdapter: workspaceStorageAdapter,
pipeline = await pipelineFactory(ctx, wsIds, true, () => {}, null) cleanIndexState,
getConnection: async () => {
if (pipeline === undefined) {
pipeline = await pipelineFactory(ctx, wsIds, true, () => {}, null)
}
return wrapPipeline(ctx, pipeline, wsIds)
},
progress: (progress) => {
return notify?.(progress) ?? Promise.resolve()
} }
return wrapPipeline(ctx, pipeline, wsIds) }),
}, { workspace: wsIds.uuid }
progress: (progress) => {
return notify?.(progress) ?? Promise.resolve()
}
})
) )
return result return result
} catch (err: any) { } catch (err: any) {

View File

@ -472,9 +472,9 @@ export function start (
return return
} }
let blobInfo = await ctx.with('stat', { workspace: wsIds.uuid }, (ctx) => let blobInfo = await ctx.with('stat', {}, (ctx) => config.storageAdapter.stat(ctx, wsIds, uuid), {
config.storageAdapter.stat(ctx, wsIds, uuid) workspace: wsIds.uuid
) })
if (blobInfo === undefined) { if (blobInfo === undefined) {
ctx.error('No such key', { file: uuid, workspace: wsIds.uuid }) ctx.error('No such key', { file: uuid, workspace: wsIds.uuid })

View File

@ -32,16 +32,26 @@ export function createPostgreeDestroyAdapter (url: string): WorkspaceDestroyAdap
} }
const connection = await client.getClient() const connection = await client.getClient()
await ctx.with('delete-workspace', {}, async () => { await ctx.with(
// We need to clear information about workspace from all collections in schema 'delete-workspace',
for (const [domain] of Object.entries(domainSchemas)) { {},
await ctx.with('delete-workspace-domain', {}, async () => { async (ctx) => {
await retryTxn(connection, async (client) => { // We need to clear information about workspace from all collections in schema
await client.unsafe(`delete from ${domain} where "workspaceId" = $1::uuid`, [workspaceUuid]) for (const [domain] of Object.entries(domainSchemas)) {
}) await ctx.with(
}) 'delete-workspace-domain',
} {},
}) async (ctx) => {
await retryTxn(connection, async (client) => {
await client.unsafe(`delete from ${domain} where "workspaceId" = $1::uuid`, [workspaceUuid])
})
},
{ domain }
)
}
},
{ url: workspaceUuid }
)
} catch (err: any) { } catch (err: any) {
ctx.error('failed to clean workspace data', { err }) ctx.error('failed to clean workspace data', { err })
throw err throw err

View File

@ -39,7 +39,7 @@ import core, {
type Ref, type Ref,
type WithLookup type WithLookup
} from '@hcengineering/core' } from '@hcengineering/core'
import { consoleModelLogger, MigrateOperation, ModelLogger, tryMigrate } from '@hcengineering/model' import { consoleModelLogger, MigrateOperation, ModelLogger, tryMigrate, type MigrateMode } from '@hcengineering/model'
import { DomainIndexHelperImpl, Pipeline, StorageAdapter, type DbAdapter } from '@hcengineering/server-core' import { DomainIndexHelperImpl, Pipeline, StorageAdapter, type DbAdapter } from '@hcengineering/server-core'
import { InitScript, WorkspaceInitializer } from './initializer' import { InitScript, WorkspaceInitializer } from './initializer'
import toolPlugin from './plugin' import toolPlugin from './plugin'
@ -150,7 +150,8 @@ export async function updateModel (
connection: TxOperations, connection: TxOperations,
pipeline: Pipeline, pipeline: Pipeline,
logger: ModelLogger = consoleModelLogger, logger: ModelLogger = consoleModelLogger,
progress: (value: number) => Promise<void> progress: (value: number) => Promise<void>,
mode: MigrateMode
): Promise<void> { ): Promise<void> {
logger.log('connecting to transactor', { workspaceId }) logger.log('connecting to transactor', { workspaceId })
@ -167,7 +168,7 @@ export async function updateModel (
let i = 0 let i = 0
for (const op of migrateOperations) { for (const op of migrateOperations) {
const st = platformNow() const st = platformNow()
await op[1].upgrade(migrateState, async () => connection as any, logger) await op[1].upgrade(migrateState, async () => connection as any, 'upgrade')
const tdelta = platformNowDiff(st) const tdelta = platformNowDiff(st)
if (tdelta > 0.5) { if (tdelta > 0.5) {
logger.log('Create', { name: op[0], time: tdelta }) logger.log('Create', { name: op[0], time: tdelta })
@ -244,7 +245,8 @@ export async function upgradeModel (
migrateOperations: [string, MigrateOperation][], migrateOperations: [string, MigrateOperation][],
logger: ModelLogger = consoleModelLogger, logger: ModelLogger = consoleModelLogger,
progress: (value: number) => Promise<void>, progress: (value: number) => Promise<void>,
updateIndexes: 'perform' | 'skip' | 'disable' = 'skip' updateIndexes: 'perform' | 'skip' | 'disable' = 'skip',
mode: MigrateMode = 'create'
): Promise<Tx[]> { ): Promise<Tx[]> {
if (txes.some((tx) => tx.objectSpace !== core.space.Model)) { if (txes.some((tx) => tx.objectSpace !== core.space.Model)) {
throw Error('Model txes must target only core.space.Model') throw Error('Model txes must target only core.space.Model')
@ -276,7 +278,7 @@ export async function upgradeModel (
const t = platformNow() const t = platformNow()
try { try {
await ctx.with(op[0], {}, (ctx) => preMigrate(preMigrateClient, logger)) await ctx.with(op[0], {}, (ctx) => preMigrate(preMigrateClient, logger, mode))
} catch (err: any) { } catch (err: any) {
logger.error(`error during pre-migrate: ${op[0]} ${err.message}`, err) logger.error(`error during pre-migrate: ${op[0]} ${err.message}`, err)
throw err throw err
@ -320,7 +322,7 @@ export async function upgradeModel (
for (const op of migrateOperations) { for (const op of migrateOperations) {
try { try {
const t = platformNow() const t = platformNow()
await ctx.with(op[0], {}, () => op[1].migrate(migrateClient, logger)) await ctx.with(op[0], {}, () => op[1].migrate(migrateClient, mode))
const tdelta = platformNowDiff(t) const tdelta = platformNowDiff(t)
if (tdelta > 0) { if (tdelta > 0) {
logger.log('migrate:', { workspaceId: wsIds, operation: op[0], time: tdelta }) logger.log('migrate:', { workspaceId: wsIds, operation: op[0], time: tdelta })
@ -349,7 +351,7 @@ export async function upgradeModel (
let i = 0 let i = 0
for (const op of migrateOperations) { for (const op of migrateOperations) {
const t = Date.now() const t = Date.now()
await ctx.with(op[0], {}, () => op[1].upgrade(migrateState, async () => connection, logger)) await ctx.with(op[0], {}, () => op[1].upgrade(migrateState, async () => connection, mode))
const tdelta = Date.now() - t const tdelta = Date.now() - t
if (tdelta > 0) { if (tdelta > 0) {
logger.log('upgrade:', { operation: op[0], time: tdelta, workspaceId: wsIds }) logger.log('upgrade:', { operation: op[0], time: tdelta, workspaceId: wsIds })

View File

@ -181,17 +181,12 @@ export class WorkspaceWorker {
await this.doSleep(ctx, opt) await this.doSleep(ctx, opt)
} else { } else {
void this.exec(async () => { void this.exec(async () => {
await this.doWorkspaceOperation( await ctx
ctx.newChild('workspaceOperation', { .with('workspaceOperation', {}, (ctx) => this.doWorkspaceOperation(ctx, workspace, opt))
workspace: workspace.uuid, .catch((err) => {
workspaceName: workspace.name Analytics.handleError(err)
}), ctx.error('error', { err })
workspace, })
opt
).catch((err) => {
Analytics.handleError(err)
ctx.error('error', { err })
})
}) })
} }
} }

View File

@ -14,7 +14,7 @@ import core, {
type WorkspaceIds, type WorkspaceIds,
type WorkspaceInfoWithStatus type WorkspaceInfoWithStatus
} from '@hcengineering/core' } from '@hcengineering/core'
import { consoleModelLogger, type MigrateOperation, type ModelLogger } from '@hcengineering/model' import { consoleModelLogger, type MigrateMode, type MigrateOperation, type ModelLogger } from '@hcengineering/model'
import { getTransactorEndpoint } from '@hcengineering/server-client' import { getTransactorEndpoint } from '@hcengineering/server-client'
import { SessionDataImpl, wrapPipeline, type Pipeline, type StorageAdapter } from '@hcengineering/server-core' import { SessionDataImpl, wrapPipeline, type Pipeline, type StorageAdapter } from '@hcengineering/server-core'
import { getServerPipeline, getTxAdapterFactory, sharedPipelineContextVars } from '@hcengineering/server-pipeline' import { getServerPipeline, getTxAdapterFactory, sharedPipelineContextVars } from '@hcengineering/server-pipeline'
@ -42,7 +42,7 @@ export async function createWorkspace (
) => Promise<void>, ) => Promise<void>,
external: boolean = false external: boolean = false
): Promise<void> { ): Promise<void> {
const childLogger = ctx.newChild('createWorkspace', {}, { workspace: workspaceInfo.uuid }) const childLogger = ctx.newChild('createWorkspace', {}, {})
const ctxModellogger: ModelLogger = { const ctxModellogger: ModelLogger = {
log: (msg, data) => { log: (msg, data) => {
childLogger.info(msg, data) childLogger.info(msg, data)
@ -96,9 +96,18 @@ export async function createWorkspace (
const client = new TxOperations(wrapPipeline(ctx, pipeline, wsIds), core.account.ConfigUser) const client = new TxOperations(wrapPipeline(ctx, pipeline, wsIds), core.account.ConfigUser)
await updateModel(ctx, wsId, migrationOperation, client, pipeline, ctxModellogger, async (value) => { await updateModel(
await handleWsEvent?.('progress', version, 10 + Math.round((Math.min(value, 100) / 100) * 10)) childLogger,
}) wsId,
migrationOperation,
client,
pipeline,
ctxModellogger,
async (value) => {
await handleWsEvent?.('progress', version, 10 + Math.round((Math.min(value, 100) / 100) * 10))
},
'create'
)
ctx.info('Starting init script if any') ctx.info('Starting init script if any')
const creatorUuid = workspaceInfo.createdBy const creatorUuid = workspaceInfo.createdBy
@ -108,7 +117,7 @@ export async function createWorkspace (
if (personInfo?.socialIds.length > 0) { if (personInfo?.socialIds.length > 0) {
await initializeWorkspace( await initializeWorkspace(
ctx, childLogger,
branding, branding,
wsIds, wsIds,
personInfo, personInfo,
@ -128,7 +137,7 @@ export async function createWorkspace (
} }
await upgradeWorkspaceWith( await upgradeWorkspaceWith(
ctx, childLogger,
version, version,
txes, txes,
migrationOperation, migrationOperation,
@ -144,7 +153,8 @@ export async function createWorkspace (
}, },
false, false,
'disable', 'disable',
external external,
'create'
) )
await handleWsEvent?.('create-done', version, 100, '') await handleWsEvent?.('create-done', version, 100, '')
@ -222,7 +232,8 @@ export async function upgradeWorkspace (
handleWsEvent, handleWsEvent,
forceUpdate, forceUpdate,
forceIndexes ? 'perform' : 'skip', forceIndexes ? 'perform' : 'skip',
external external,
'upgrade'
) )
} finally { } finally {
await pipeline?.close() await pipeline?.close()
@ -252,7 +263,8 @@ export async function upgradeWorkspaceWith (
) => Promise<void>, ) => Promise<void>,
forceUpdate: boolean = true, forceUpdate: boolean = true,
updateIndexes: 'perform' | 'skip' | 'disable' = 'skip', updateIndexes: 'perform' | 'skip' | 'disable' = 'skip',
external: boolean = false external: boolean = false,
mode: MigrateMode = 'create'
): Promise<void> { ): Promise<void> {
const versionStr = versionToString(version) const versionStr = versionToString(version)
const workspaceVersion = { const workspaceVersion = {
@ -315,7 +327,8 @@ export async function upgradeWorkspaceWith (
async (value) => { async (value) => {
progress = value progress = value
}, },
updateIndexes updateIndexes,
mode
) )
await handleWsEvent?.('upgrade-done', version, 100, '') await handleWsEvent?.('upgrade-done', version, 100, '')

View File

@ -379,9 +379,9 @@ export function startHttpServer (
const range = req.headers.range const range = req.headers.range
if (range !== undefined) { if (range !== undefined) {
ctx ctx
.with('file-range', { workspace: wsIds.uuid }, (ctx) => .with('file-range', {}, (ctx) => getFileRange(ctx, range, externalStorage, wsIds, name, wrapRes(res)), {
getFileRange(ctx, range, externalStorage, wsIds, name, wrapRes(res)) workspace: wsIds.uuid
) })
.catch((err) => { .catch((err) => {
Analytics.handleError(err) Analytics.handleError(err)
ctx.error('/api/v1/blob get error', { err }) ctx.error('/api/v1/blob get error', { err })

View File

@ -93,6 +93,7 @@ services:
- STATS_URL=http://huly.local:4901 - STATS_URL=http://huly.local:4901
- BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG} - BACKUP_STORAGE=${BACKUP_STORAGE_CONFIG}
- BACKUP_BUCKET=${BACKUP_BUCKET_NAME} - BACKUP_BUCKET=${BACKUP_BUCKET_NAME}
# - INIT_WORKSPACE=huly
restart: unless-stopped restart: unless-stopped
workspace_europe: workspace_europe:
image: hardcoreeng/workspace image: hardcoreeng/workspace

5
ws-tests/prepare_data.sh Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
./tool.sh create-workspace asa -w asa
./tool.sh backup-restore ${DUMP_ROOT}/asa asa --skip blob
./tool.sh assign-workspace user1 asa