mirror of
https://github.com/hcengineering/platform.git
synced 2026-10-01 14:05:05 +02:00
Fix controlled doc sequence conflicts (#10406)
* Fix document sequence conflicts Signed-off-by: Artem Savchenko <armisav@gmail.com> * Clean up Signed-off-by: Artem Savchenko <armisav@gmail.com> --------- Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -531,6 +531,17 @@ export class ApplyOperations extends TxOperations {
|
||||
if (typeof window === 'object' && window !== null && this.measureName != null) {
|
||||
console.log(`measure ${this.measureName}`, dnow - st, 'server time', result.serverTime)
|
||||
}
|
||||
if (!result.success) {
|
||||
console.warn('ops.commit() failed', {
|
||||
scope: this.scope,
|
||||
measureName: this.measureName,
|
||||
matchCount: this.matches.length,
|
||||
notMatchCount: this.notMatches.length,
|
||||
txCount: this.txes.length,
|
||||
matches: this.matches.map((m) => ({ _class: m._class, query: m.query })),
|
||||
notMatches: this.notMatches.map((m) => ({ _class: m._class, query: m.query }))
|
||||
})
|
||||
}
|
||||
this.txes = []
|
||||
return {
|
||||
result: result.success,
|
||||
|
||||
@@ -63,6 +63,14 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
}
|
||||
applyResult.serverTime = Date.now() - st
|
||||
} else {
|
||||
ctx.warn('TxApplyIf failed', {
|
||||
scope: applyIf.scope,
|
||||
reason: passed.reason,
|
||||
measureName: applyIf.measureName,
|
||||
matchCount: applyIf.match?.length ?? 0,
|
||||
notMatchCount: applyIf.notMatch?.length ?? 0,
|
||||
txCount: applyIf.txes.length
|
||||
})
|
||||
result.push({
|
||||
success: false
|
||||
})
|
||||
@@ -92,6 +100,7 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
): Promise<{
|
||||
onEnd: () => void
|
||||
passed: boolean
|
||||
reason?: string
|
||||
}> {
|
||||
if (applyIf.scope == null) {
|
||||
return { passed: true, onEnd: () => {} }
|
||||
@@ -115,11 +124,13 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
})
|
||||
)
|
||||
let passed = true
|
||||
let reason: string | undefined
|
||||
if (applyIf.match != null) {
|
||||
for (const { _class, query } of applyIf.match) {
|
||||
const res = await this.provideFindAll(ctx, _class, query, { limit: 1 })
|
||||
if (res.length === 0) {
|
||||
passed = false
|
||||
reason = `match query failed: class=${_class}, query=${JSON.stringify(query)}`
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -129,10 +140,11 @@ export class ApplyTxMiddleware extends BaseMiddleware implements Middleware {
|
||||
const res = await this.provideFindAll(ctx, _class, query, { limit: 1 })
|
||||
if (res.length > 0) {
|
||||
passed = false
|
||||
reason = `notMatch query failed: class=${_class}, query=${JSON.stringify(query)} (found ${res.length} matching document(s))`
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return { passed, onEnd }
|
||||
return { passed, onEnd, reason }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import { makeRank } from '@hcengineering/rank'
|
||||
|
||||
import documents from './plugin'
|
||||
import { getFirstRank, TEMPLATE_PREFIX } from './utils'
|
||||
import { getDocumentId, getFirstRank, TEMPLATE_PREFIX } from './utils'
|
||||
|
||||
async function getParentPath (client: TxOperations, parent: Ref<ProjectDocument>): Promise<Array<Ref<DocumentMeta>>> {
|
||||
const parentDocObj = await client.findOne(documents.class.ProjectDocument, {
|
||||
@@ -68,8 +68,10 @@ export async function createControlledDocFromTemplate (
|
||||
return { seqNumber: -1, success: false }
|
||||
}
|
||||
|
||||
const { seqNumber, prefix, content, category } = await useDocumentTemplate(client, templateId)
|
||||
const { success, documentMetaId } = await createControlledDocMetadata(
|
||||
// Try fast path first (assumes template sequence is in sync)
|
||||
let { seqNumber, prefix, content, category } = await useDocumentTemplate(client, templateId, false)
|
||||
let actualCode = getDocumentId({ prefix, seqNumber })
|
||||
let { success, documentMetaId } = await createControlledDocMetadata(
|
||||
client,
|
||||
templateId,
|
||||
documentId,
|
||||
@@ -78,10 +80,35 @@ export async function createControlledDocFromTemplate (
|
||||
parent,
|
||||
prefix,
|
||||
seqNumber,
|
||||
spec.code,
|
||||
actualCode,
|
||||
spec.title
|
||||
)
|
||||
|
||||
// If creation failed due to seqNumber conflict, retry with full uniqueness check
|
||||
if (!success) {
|
||||
// Retry with expensive check to find actual max seqNumber
|
||||
const retryResult = await useDocumentTemplate(client, templateId, true)
|
||||
seqNumber = retryResult.seqNumber
|
||||
prefix = retryResult.prefix
|
||||
content = retryResult.content
|
||||
category = retryResult.category
|
||||
actualCode = getDocumentId({ prefix, seqNumber })
|
||||
const retryMetadata = await createControlledDocMetadata(
|
||||
client,
|
||||
templateId,
|
||||
documentId,
|
||||
space,
|
||||
project,
|
||||
parent,
|
||||
prefix,
|
||||
seqNumber,
|
||||
actualCode,
|
||||
spec.title
|
||||
)
|
||||
success = retryMetadata.success
|
||||
documentMetaId = retryMetadata.documentMetaId
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
return { seqNumber: -1, success: false }
|
||||
}
|
||||
@@ -107,9 +134,33 @@ export async function createControlledDocFromTemplate (
|
||||
return { seqNumber, success: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the next available seqNumber by checking existing documents with the template.
|
||||
*/
|
||||
async function calculateNextSeqNumberWithCheck (
|
||||
client: TxOperations,
|
||||
templateId: Ref<DocumentTemplate>,
|
||||
currentTemplateSequence: number
|
||||
): Promise<number> {
|
||||
const existingDocs = await client.findAll(
|
||||
documents.class.Document,
|
||||
{
|
||||
template: templateId
|
||||
},
|
||||
{
|
||||
projection: { seqNumber: 1 }
|
||||
}
|
||||
)
|
||||
|
||||
const maxExistingSeqNumber = existingDocs.length > 0 ? Math.max(...existingDocs.map((doc) => doc.seqNumber ?? 0)) : -1
|
||||
|
||||
return Math.max(currentTemplateSequence, maxExistingSeqNumber) + 1
|
||||
}
|
||||
|
||||
export async function useDocumentTemplate (
|
||||
client: TxOperations,
|
||||
templateId: Ref<DocumentTemplate>
|
||||
templateId: Ref<DocumentTemplate>,
|
||||
checkExisting: boolean = false
|
||||
): Promise<{ seqNumber: number, prefix: string, content: Ref<Blob> | null, category: Ref<DocumentCategory> }> {
|
||||
const template = await client.findOne(documents.mixin.DocumentTemplate, {
|
||||
_id: templateId
|
||||
@@ -119,15 +170,27 @@ export async function useDocumentTemplate (
|
||||
return { seqNumber: -1, prefix: '', content: null, category: '' as Ref<DocumentCategory> }
|
||||
}
|
||||
|
||||
let nextSeqNumber: number
|
||||
|
||||
if (checkExisting) {
|
||||
nextSeqNumber = await calculateNextSeqNumberWithCheck(client, templateId, template.sequence)
|
||||
} else {
|
||||
nextSeqNumber = template.sequence + 1
|
||||
}
|
||||
|
||||
// Update template sequence to nextSeqNumber in a single atomic operation
|
||||
await client.updateMixin(templateId, documents.class.Document, template.space, documents.mixin.DocumentTemplate, {
|
||||
$inc: { sequence: 1 }
|
||||
sequence: nextSeqNumber
|
||||
})
|
||||
|
||||
// FIXME: not concurrency safe
|
||||
const seqNumber = template.sequence + 1
|
||||
const prefix = template.docPrefix
|
||||
|
||||
return { seqNumber, prefix, content: template.content, category: template.category as Ref<DocumentCategory> }
|
||||
return {
|
||||
seqNumber: nextSeqNumber,
|
||||
prefix,
|
||||
content: template.content,
|
||||
category: template.category as Ref<DocumentCategory>
|
||||
}
|
||||
}
|
||||
|
||||
export async function createControlledDocMetadata (
|
||||
@@ -203,6 +266,22 @@ export async function createControlledDocMetadata (
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
if (!success.result) {
|
||||
console.warn('createControlledDocMetadata: ops.commit() failed', {
|
||||
templateId,
|
||||
documentId,
|
||||
space,
|
||||
project,
|
||||
parent,
|
||||
prefix,
|
||||
seqNumber,
|
||||
specCode,
|
||||
specTitle,
|
||||
documentMetaId,
|
||||
projectDocumentId
|
||||
})
|
||||
}
|
||||
|
||||
return { success: success.result, seqNumber, documentMetaId, projectDocumentId }
|
||||
}
|
||||
|
||||
@@ -261,6 +340,20 @@ export async function createDocumentTemplate (
|
||||
})
|
||||
const commit = await ops.commit()
|
||||
|
||||
if (!commit.result) {
|
||||
console.warn('createDocumentTemplate: ops.commit() failed', {
|
||||
_class,
|
||||
space,
|
||||
_mixin,
|
||||
project,
|
||||
parent,
|
||||
templateId,
|
||||
prefix,
|
||||
category,
|
||||
author
|
||||
})
|
||||
}
|
||||
|
||||
return { seqNumber, success: commit.result }
|
||||
}
|
||||
|
||||
@@ -355,6 +448,24 @@ export async function createDocumentTemplateMetadata (
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
if (!success.result) {
|
||||
console.warn('createDocumentTemplateMetadata: ops.commit() failed', {
|
||||
_class,
|
||||
space,
|
||||
_mixin,
|
||||
project,
|
||||
parent,
|
||||
templateId,
|
||||
prefix,
|
||||
specCode,
|
||||
specTitle,
|
||||
seqNumber,
|
||||
code,
|
||||
documentMetaId,
|
||||
projectDocumentId
|
||||
})
|
||||
}
|
||||
|
||||
return { success: success.result, seqNumber, code, documentMetaId, projectDocumentId }
|
||||
}
|
||||
|
||||
@@ -407,5 +518,16 @@ export async function createNewFolder (
|
||||
|
||||
const success = await ops.commit()
|
||||
|
||||
if (!success.result) {
|
||||
console.warn('createNewFolder: ops.commit() failed', {
|
||||
space,
|
||||
project,
|
||||
parent,
|
||||
title,
|
||||
documentMetaId,
|
||||
projectDocumentId
|
||||
})
|
||||
}
|
||||
|
||||
return { success: success.result, documentMetaId, projectDocumentId }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user