UBERF-7260 Handle storage errors in collaborator (#5806)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-06-13 23:27:35 +07:00 committed by GitHub
parent 7b466b8410
commit ce4070412f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 90 additions and 68 deletions

View File

@ -17,5 +17,5 @@ export * from './history/branch'
export * from './history/history' export * from './history/history'
export * from './history/snapshot' export * from './history/snapshot'
export * from './utils/collaborative-doc' export * from './utils/collaborative-doc'
export * from './utils/minio' export * from './utils/storage'
export * from './utils/ydoc' export * from './utils/ydoc'

View File

@ -28,7 +28,7 @@ import { StorageAdapter } from '@hcengineering/server-core'
import { yDocBranch } from '../history/branch' import { yDocBranch } from '../history/branch'
import { YDocVersion } from '../history/history' import { YDocVersion } from '../history/history'
import { createYdocSnapshot, restoreYdocSnapshot } from '../history/snapshot' import { createYdocSnapshot, restoreYdocSnapshot } from '../history/snapshot'
import { yDocFromStorage, yDocToStorage } from './minio' import { yDocFromStorage, yDocToStorage } from './storage'
/** @public */ /** @public */
export function collaborativeHistoryDocId (id: string): string { export function collaborativeHistoryDocId (id: string): string {

View File

@ -27,19 +27,18 @@ export async function yDocFromStorage (
minioDocumentId: string, minioDocumentId: string,
ydoc?: YDoc ydoc?: YDoc
): Promise<YDoc | undefined> { ): Promise<YDoc | undefined> {
// stat the object to ensure it exists, because read will throw an error in this case
const blob = await storageAdapter.stat(ctx, workspace, minioDocumentId)
if (blob === undefined) {
return undefined
}
// no need to apply gc because we load existing document // no need to apply gc because we load existing document
// it is either already gc-ed, or gc not needed and it is disabled // it is either already gc-ed, or gc not needed and it is disabled
ydoc ??= new YDoc({ gc: false }) ydoc ??= new YDoc({ gc: false })
try {
const buffer = await storageAdapter.read(ctx, workspace, minioDocumentId) const buffer = await storageAdapter.read(ctx, workspace, minioDocumentId)
return yDocFromBuffer(Buffer.concat(buffer), ydoc) return yDocFromBuffer(Buffer.concat(buffer), ydoc)
} catch (err: any) {
if (err?.code === 'NoSuchKey' || err?.code === 'NotFound') {
return undefined
}
throw err
}
} }
/** @public */ /** @public */

View File

@ -107,8 +107,8 @@ export class StorageExtension implements Extension {
return await adapter.loadDocument(ctx, documentId, context) return await adapter.loadDocument(ctx, documentId, context)
}) })
} catch (err) { } catch (err) {
ctx.error('failed to load document content', { documentId, error: err }) ctx.error('failed to load document', { documentId, error: err })
return undefined throw new Error('Failed to load document')
} }
} }
@ -120,8 +120,8 @@ export class StorageExtension implements Extension {
await adapter.saveDocument(ctx, documentId, document, context) await adapter.saveDocument(ctx, documentId, document, context)
}) })
} catch (err) { } catch (err) {
ctx.error('failed to save document content', { documentId, error: err }) ctx.error('failed to save document', { documentId, error: err })
return undefined throw new Error('Failed to save document')
} }
} }
} }

View File

@ -52,7 +52,6 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
) {} ) {}
async loadDocument (ctx: MeasureContext, documentId: DocumentId, context: Context): Promise<YDoc | undefined> { async loadDocument (ctx: MeasureContext, documentId: DocumentId, context: Context): Promise<YDoc | undefined> {
try {
// try to load document content // try to load document content
try { try {
ctx.info('load document content', { documentId }) ctx.info('load document content', { documentId })
@ -63,6 +62,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
} }
} catch (err) { } catch (err) {
ctx.error('failed to load document content', { documentId, error: err }) ctx.error('failed to load document content', { documentId, error: err })
throw err
} }
// then try to load from inital content // then try to load from inital content
@ -81,6 +81,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
} }
} catch (err) { } catch (err) {
ctx.error('failed to load initial document content', { documentId, initialContentId, error: err }) ctx.error('failed to load initial document content', { documentId, initialContentId, error: err })
throw err
} }
} }
@ -93,6 +94,7 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
return await this.loadDocumentFromPlatform(ctx, platformDocumentId, context) return await this.loadDocumentFromPlatform(ctx, platformDocumentId, context)
} catch (err) { } catch (err) {
ctx.error('failed to load platform document', { documentId, platformDocumentId, error: err }) ctx.error('failed to load platform document', { documentId, platformDocumentId, error: err })
throw err
} }
}) })
@ -107,9 +109,6 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
// nothing found // nothing found
return undefined return undefined
} catch (err) {
ctx.error('failed to load document', { documentId, error: err })
}
} }
async saveDocument (ctx: MeasureContext, documentId: DocumentId, document: YDoc, context: Context): Promise<void> { async saveDocument (ctx: MeasureContext, documentId: DocumentId, document: YDoc, context: Context): Promise<void> {
@ -133,6 +132,9 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
await this.saveDocumentToStorage(ctx, documentId, document, context) await this.saveDocumentToStorage(ctx, documentId, document, context)
} catch (err) { } catch (err) {
ctx.error('failed to save document', { documentId, error: err }) ctx.error('failed to save document', { documentId, error: err })
// raise an error if failed to save document to storage
// this will prevent document from being unloaded from memory
throw err
} }
const { platformDocumentId } = context const { platformDocumentId } = context
@ -166,12 +168,9 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
const adapter = this.getStorageAdapter(storage) const adapter = this.getStorageAdapter(storage)
return await ctx.with('load-document', { storage }, async (ctx) => { return await ctx.with('load-document', { storage }, async (ctx) => {
try { return await withRetry(ctx, 5, async () => {
return await loadCollaborativeDoc(adapter, context.workspaceId, collaborativeDoc, ctx) return await loadCollaborativeDoc(adapter, context.workspaceId, collaborativeDoc, ctx)
} catch (err) { })
ctx.error('failed to load storage document', { documentId, collaborativeDoc, error: err })
return undefined
}
}) })
} }
@ -185,8 +184,10 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
const adapter = this.getStorageAdapter(storage) const adapter = this.getStorageAdapter(storage)
await ctx.with('save-document', {}, async (ctx) => { await ctx.with('save-document', {}, async (ctx) => {
await withRetry(ctx, 5, async () => {
await saveCollaborativeDoc(adapter, context.workspaceId, collaborativeDoc, document, ctx) await saveCollaborativeDoc(adapter, context.workspaceId, collaborativeDoc, document, ctx)
}) })
})
} }
async takeSnapshot ( async takeSnapshot (
@ -291,3 +292,25 @@ export class PlatformStorageAdapter implements CollabStorageAdapter {
} }
} }
} }
async function withRetry<T> (
ctx: MeasureContext,
retries: number,
op: () => Promise<T>,
delay: number = 100
): Promise<T> {
let error: any
while (retries > 0) {
retries--
try {
return await op()
} catch (err: any) {
error = err
ctx.error('error', err)
if (retries !== 0) {
await new Promise((resolve) => setTimeout(resolve, delay))
}
}
}
throw error
}