mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 12:35:02 +02:00
UBERF-4319: Trigger Server queries (#4550)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -108,6 +108,7 @@ export class TDoc extends TObj implements Doc {
|
||||
|
||||
@Prop(TypeTimestamp(), core.string.CreatedDate)
|
||||
@ReadOnly()
|
||||
@Index(IndexKind.IndexedDsc)
|
||||
createdOn!: Timestamp
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
// Basic performance metrics suite.
|
||||
|
||||
import { childMetrics, measure, newMetrics } from './metrics'
|
||||
import { MeasureContext, MeasureLogger, Metrics, ParamType } from './types'
|
||||
import { FullParamsType, MeasureContext, MeasureLogger, Metrics, ParamsType } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export class MeasureMetricsContext implements MeasureContext {
|
||||
private readonly name: string
|
||||
private readonly params: Record<string, ParamType>
|
||||
private readonly params: ParamsType
|
||||
logger: MeasureLogger
|
||||
metrics: Metrics
|
||||
private readonly done: (value?: number) => void
|
||||
|
||||
constructor (
|
||||
name: string,
|
||||
params: Record<string, ParamType>,
|
||||
params: ParamsType,
|
||||
fullParams: FullParamsType = {},
|
||||
metrics: Metrics = newMetrics(),
|
||||
logger?: MeasureLogger
|
||||
) {
|
||||
this.name = name
|
||||
this.params = params
|
||||
this.metrics = metrics
|
||||
this.done = measure(metrics, params)
|
||||
this.done = measure(metrics, params, fullParams)
|
||||
|
||||
this.logger = logger ?? {
|
||||
info: (msg, args) => {
|
||||
@@ -35,20 +36,21 @@ export class MeasureMetricsContext implements MeasureContext {
|
||||
}
|
||||
|
||||
measure (name: string, value: number): void {
|
||||
const c = new MeasureMetricsContext('#' + name, {}, childMetrics(this.metrics, ['#' + name]))
|
||||
const c = new MeasureMetricsContext('#' + name, {}, {}, childMetrics(this.metrics, ['#' + name]))
|
||||
c.done(value)
|
||||
}
|
||||
|
||||
newChild (name: string, params: Record<string, ParamType>, logger?: MeasureLogger): MeasureContext {
|
||||
return new MeasureMetricsContext(name, params, childMetrics(this.metrics, [name]), logger)
|
||||
newChild (name: string, params: ParamsType, fullParams?: FullParamsType, logger?: MeasureLogger): MeasureContext {
|
||||
return new MeasureMetricsContext(name, params, fullParams ?? {}, childMetrics(this.metrics, [name]), logger)
|
||||
}
|
||||
|
||||
async with<T>(
|
||||
name: string,
|
||||
params: Record<string, ParamType>,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>,
|
||||
fullParams?: ParamsType
|
||||
): Promise<T> {
|
||||
const c = this.newChild(name, params)
|
||||
const c = this.newChild(name, params, fullParams)
|
||||
try {
|
||||
let value = op(c)
|
||||
if (value instanceof Promise) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Basic performance metrics suite.
|
||||
|
||||
import { MetricsData } from '.'
|
||||
import { Metrics, ParamType } from './types'
|
||||
import { cutObjectArray } from '../utils'
|
||||
import { FullParamsType, Metrics, ParamsType } from './types'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -21,11 +22,30 @@ export function newMetrics (): Metrics {
|
||||
}
|
||||
}
|
||||
|
||||
function getUpdatedTopResult (
|
||||
current: Metrics['topResult'],
|
||||
time: number,
|
||||
params: FullParamsType
|
||||
): Metrics['topResult'] {
|
||||
if (current === undefined || current.length < 3 || current.some((it) => it.value < time)) {
|
||||
const result = [
|
||||
...(current ?? []),
|
||||
{
|
||||
value: time,
|
||||
params: cutObjectArray(params)
|
||||
}
|
||||
]
|
||||
result.sort((a, b) => b.value - a.value)
|
||||
return result.slice(0, 3)
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure with tree expansion. Operation counter will be added only to leaf's.
|
||||
* @public
|
||||
*/
|
||||
export function measure (metrics: Metrics, params: Record<string, ParamType>): () => void {
|
||||
export function measure (metrics: Metrics, params: ParamsType, fullParams: FullParamsType = {}): () => void {
|
||||
const st = Date.now()
|
||||
return (value?: number) => {
|
||||
const ed = Date.now()
|
||||
@@ -47,10 +67,14 @@ export function measure (metrics: Metrics, params: Record<string, ParamType>): (
|
||||
}
|
||||
param.value += value ?? ed - st
|
||||
param.operations++
|
||||
|
||||
param.topResult = getUpdatedTopResult(param.topResult, ed - st, fullParams)
|
||||
}
|
||||
// Update leaf data
|
||||
metrics.value += value ?? ed - st
|
||||
metrics.operations++
|
||||
|
||||
metrics.topResult = getUpdatedTopResult(metrics.topResult, ed - st, fullParams)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,11 +108,13 @@ export function metricsAggregate (m: Metrics): Metrics {
|
||||
.reduce((p, v) => {
|
||||
return p + v.value
|
||||
}, 0)
|
||||
|
||||
return {
|
||||
operations: m.operations,
|
||||
measurements: ms,
|
||||
params: m.params,
|
||||
value: sumVal
|
||||
value: sumVal,
|
||||
topResult: m.topResult
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,26 @@
|
||||
*/
|
||||
export type ParamType = string | number | boolean | undefined
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type ParamsType = Record<string, ParamType>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type FullParamsType = Record<string, any>
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface MetricsData {
|
||||
operations: number
|
||||
value: number
|
||||
topResult?: {
|
||||
value: number
|
||||
params: FullParamsType
|
||||
}[]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,9 +45,14 @@ export interface MeasureLogger {
|
||||
*/
|
||||
export interface MeasureContext {
|
||||
// Create a child metrics context
|
||||
newChild: (name: string, params: Record<string, ParamType>, logger?: MeasureLogger) => MeasureContext
|
||||
newChild: (name: string, params: ParamsType, fullParams?: FullParamsType, logger?: MeasureLogger) => MeasureContext
|
||||
|
||||
with: <T>(name: string, params: Record<string, ParamType>, op: (ctx: MeasureContext) => T | Promise<T>) => Promise<T>
|
||||
with: <T>(
|
||||
name: string,
|
||||
params: ParamsType,
|
||||
op: (ctx: MeasureContext) => T | Promise<T>,
|
||||
fullParams?: FullParamsType
|
||||
) => Promise<T>
|
||||
|
||||
logger: MeasureLogger
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export interface ServerStorage extends LowLevelStorage {
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> & {
|
||||
domain?: Domain // Allow to find for Doc's in specified domain only.
|
||||
prefix?: string
|
||||
}
|
||||
) => Promise<FindResult<T>>
|
||||
searchFulltext: (ctx: MeasureContext, query: SearchQuery, options: SearchOptions) => Promise<SearchResult>
|
||||
|
||||
+47
-15
@@ -15,29 +15,25 @@
|
||||
|
||||
import core, {
|
||||
AttachedDoc,
|
||||
checkMixinKey,
|
||||
BulkUpdateEvent,
|
||||
Class,
|
||||
Client,
|
||||
DOMAIN_MODEL,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
DOMAIN_MODEL,
|
||||
FindOptions,
|
||||
findProperty,
|
||||
FindResult,
|
||||
generateId,
|
||||
getObjectValue,
|
||||
Hierarchy,
|
||||
IndexingUpdateEvent,
|
||||
BulkUpdateEvent,
|
||||
Lookup,
|
||||
LookupData,
|
||||
matchQuery,
|
||||
ModelDb,
|
||||
Ref,
|
||||
resultSort,
|
||||
ReverseLookups,
|
||||
SearchOptions,
|
||||
SearchQuery,
|
||||
SearchResult,
|
||||
SortingQuery,
|
||||
toFindResult,
|
||||
Tx,
|
||||
TxCollectionCUD,
|
||||
TxCreateDoc,
|
||||
@@ -49,9 +45,13 @@ import core, {
|
||||
TxWorkspaceEvent,
|
||||
WithLookup,
|
||||
WorkspaceEvent,
|
||||
SearchQuery,
|
||||
SearchOptions,
|
||||
SearchResult
|
||||
checkMixinKey,
|
||||
findProperty,
|
||||
generateId,
|
||||
getObjectValue,
|
||||
matchQuery,
|
||||
resultSort,
|
||||
toFindResult
|
||||
} from '@hcengineering/core'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
@@ -340,6 +340,32 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
}
|
||||
|
||||
async queryFind<T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
const current = this.findQuery(_class, query, options)
|
||||
if (current === undefined) {
|
||||
const q = this.createQuery(
|
||||
_class,
|
||||
query,
|
||||
{
|
||||
callback: () => {
|
||||
// do nothing
|
||||
},
|
||||
callbackId: generateId()
|
||||
},
|
||||
options
|
||||
)
|
||||
if (q.result instanceof Promise) {
|
||||
q.result = await q.result
|
||||
}
|
||||
return toFindResult(this.clone(q.result), q.total) as FindResult<T>
|
||||
}
|
||||
return toFindResult(this.clone((current?.result as T[]) ?? []), current.total)
|
||||
}
|
||||
|
||||
private async checkSearch (q: Query, _id: Ref<Doc>): Promise<boolean> {
|
||||
const match = await this.client.findOne(q._class, { $search: q.query.$search, _id }, q.options)
|
||||
if (q.result instanceof Promise) {
|
||||
@@ -699,17 +725,23 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
const docIdKey = doc._id + JSON.stringify(q.options?.lookup)
|
||||
const docIdKey = doc._id + JSON.stringify(q.options?.lookup) + Hierarchy.mixinClass(doc)
|
||||
|
||||
const lookup = q.options?.lookup
|
||||
const realDoc =
|
||||
docCache?.get(docIdKey) ??
|
||||
(await this.client.findOne(q._class, { _id: doc._id }, lookup !== undefined ? { lookup } : undefined))
|
||||
if (realDoc == null) return false
|
||||
|
||||
if (realDoc != null && docCache != null) {
|
||||
if (this.getHierarchy().isMixin(q._class)) {
|
||||
if (!this.getHierarchy().hasMixin(realDoc, q._class)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (docCache != null) {
|
||||
docCache?.set(docIdKey, realDoc)
|
||||
}
|
||||
if (realDoc === undefined) return false
|
||||
const res = matchQuery([realDoc], q.query, q._class, this.client.getHierarchy())
|
||||
if (res.length === 1) {
|
||||
if (q.result instanceof Promise) {
|
||||
|
||||
@@ -53,6 +53,29 @@
|
||||
</div>
|
||||
</FixedColumn>
|
||||
</svelte:fragment>
|
||||
{#if metrics.topResult !== undefined && metrics.topResult.length > 0 && metrics.topResult[0].value > 0 && Object.keys(metrics.topResult[0].params).length > 0}
|
||||
<div class="p-1" style:margin-left={`${level * 0.5 + 0.5}rem`}>
|
||||
<Expandable>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center flex-between flex-grow ml-2">
|
||||
Slowest result:{metrics.topResult[0].value}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#each metrics.topResult ?? [] as r}
|
||||
<Expandable>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center flex-between flex-grow">
|
||||
Time:{r.value}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<pre>
|
||||
{JSON.stringify(r, null, 2)}
|
||||
</pre>
|
||||
</Expandable>
|
||||
{/each}
|
||||
</Expandable>
|
||||
</div>
|
||||
{/if}
|
||||
{#each Object.entries(metrics.measurements) as [k, v], i (k)}
|
||||
<div style:margin-left={`${level * 0.5}rem`}>
|
||||
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} />
|
||||
@@ -61,7 +84,12 @@
|
||||
{#each Object.entries(metrics.params) as [k, v], i}
|
||||
<div style:margin-left={`${level * 0.5}rem`}>
|
||||
{#each Object.entries(v).toSorted((a, b) => b[1].value / (b[1].operations + 1) - a[1].value / (a[1].operations + 1)) as [kk, vv]}
|
||||
<Expandable expandable={false} bordered showChevron={false} contentColor>
|
||||
{@const childExpandable =
|
||||
vv.topResult !== undefined &&
|
||||
vv.topResult.length > 0 &&
|
||||
vv.topResult[0].value > 0 &&
|
||||
Object.keys(vv.topResult[0].params).length > 0}
|
||||
<Expandable expandable={childExpandable} bordered showChevron={childExpandable} contentColor>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center flex-between flex-grow">
|
||||
# {k} = {kk}
|
||||
@@ -71,11 +99,29 @@
|
||||
<FixedColumn key="row">
|
||||
<div class="flex-row-center flex-between">
|
||||
<FixedColumn key="ops">{vv.operations}</FixedColumn>
|
||||
<FixedColumn key="time">{showAvg(kk, vv.value, vv.operations)}</FixedColumn>
|
||||
<FixedColumn key="time">
|
||||
{showAvg(kk, vv.value, vv.operations)}
|
||||
</FixedColumn>
|
||||
<FixedColumn key="time-full">{vv.value}</FixedColumn>
|
||||
</div>
|
||||
</FixedColumn>
|
||||
</svelte:fragment>
|
||||
{#if childExpandable}
|
||||
<div class="p-1" style:margin-left={`${level * 0.5 + 0.5}rem`}>
|
||||
{#each vv.topResult ?? [] as r}
|
||||
<Expandable>
|
||||
<svelte:fragment slot="title">
|
||||
<div class="flex-row-center flex-between flex-grow">
|
||||
Time:{r.value}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<pre>
|
||||
{JSON.stringify(r, null, 2)}
|
||||
</pre>
|
||||
</Expandable>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Expandable>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"@hcengineering/core": "^0.6.28",
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
"@hcengineering/minio": "^0.6.0",
|
||||
"@hcengineering/query": "^0.6.8",
|
||||
"fast-equals": "^2.0.3",
|
||||
"html-to-text": "^9.0.3"
|
||||
}
|
||||
|
||||
+142
-135
@@ -327,16 +327,20 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
})
|
||||
|
||||
while (!this.cancelling) {
|
||||
await this.metrics.with('initialize-stages', {}, async () => {
|
||||
await this.metrics.with('initialize-stages', { workspace: this.workspace.name }, async () => {
|
||||
await this.initializeStages()
|
||||
})
|
||||
|
||||
await this.metrics.with('process-remove', {}, async () => {
|
||||
await this.metrics.with('process-remove', { workspace: this.workspace.name }, async () => {
|
||||
await this.processRemove()
|
||||
})
|
||||
|
||||
const _classes = await rateLimitter.exec(() => {
|
||||
return this.metrics.with('init-stages', {}, async () => await this.processIndex())
|
||||
return this.metrics.with(
|
||||
'processIndex',
|
||||
{ workspace: this.workspace.name },
|
||||
async (ctx) => await this.processIndex(ctx)
|
||||
)
|
||||
})
|
||||
|
||||
// Also update doc index state queries.
|
||||
@@ -374,7 +378,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
console.log(this.workspace.name, 'Exit indexer', this.indexId)
|
||||
}
|
||||
|
||||
private async processIndex (): Promise<Ref<Class<Doc>>[]> {
|
||||
private async processIndex (ctx: MeasureContext): Promise<Ref<Class<Doc>>[]> {
|
||||
let idx = 0
|
||||
const _classUpdate = new Set<Ref<Class<Doc>>>()
|
||||
for (const st of this.stages) {
|
||||
@@ -387,14 +391,14 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
if (!st.enabled) {
|
||||
break
|
||||
}
|
||||
await this.metrics.with('flush', {}, async () => {
|
||||
await ctx.with('flush', {}, async () => {
|
||||
await this.flush(true)
|
||||
})
|
||||
const toSkip = Array.from(this.skipped.entries())
|
||||
.filter((it) => it[1] > 3)
|
||||
.map((it) => it[0])
|
||||
|
||||
let result = await this.metrics.with(
|
||||
let result = await ctx.with(
|
||||
'get-to-index',
|
||||
{},
|
||||
async () =>
|
||||
@@ -460,7 +464,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
// Do Indexing
|
||||
this.currentStage = st
|
||||
|
||||
await this.metrics.with('collect', { collector: st.stageId }, async (ctx) => {
|
||||
await ctx.with('collect', { collector: st.stageId }, async (ctx) => {
|
||||
await st.collect(toIndex, this, ctx)
|
||||
})
|
||||
if (this.cancelling) {
|
||||
@@ -474,7 +478,7 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
const toIndex2 = this.matchStates(nst)
|
||||
if (toIndex2.length > 0) {
|
||||
this.currentStage = nst
|
||||
await this.metrics.with('collect', { collector: nst.stageId }, async (ctx) => {
|
||||
await ctx.with('collect', { collector: nst.stageId }, async (ctx) => {
|
||||
await nst.collect(toIndex2, this, ctx)
|
||||
})
|
||||
}
|
||||
@@ -575,152 +579,155 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
||||
// TODO: Move to migration
|
||||
async checkIndexConsistency (dbStorage: ServerStorage): Promise<void> {
|
||||
await rateLimitter.exec(async () => {
|
||||
if (process.env.MODEL_VERSION !== undefined && process.env.MODEL_VERSION !== '') {
|
||||
const modelVersion = (await this.model.findAll(core.class.Version, {}))[0]
|
||||
if (modelVersion !== undefined) {
|
||||
const modelVersionString = versionToString(modelVersion)
|
||||
if (modelVersionString !== process.env.MODEL_VERSION) {
|
||||
console.error(
|
||||
`Indexer: Model version mismatch model: ${modelVersionString} env: ${process.env.MODEL_VERSION}`
|
||||
)
|
||||
return
|
||||
await this.metrics.with('check-index-consistency', {}, async (ctx) => {
|
||||
if (process.env.MODEL_VERSION !== undefined && process.env.MODEL_VERSION !== '') {
|
||||
const modelVersion = (await this.model.findAll(core.class.Version, {}))[0]
|
||||
if (modelVersion !== undefined) {
|
||||
const modelVersionString = versionToString(modelVersion)
|
||||
if (modelVersionString !== process.env.MODEL_VERSION) {
|
||||
console.error(
|
||||
`Indexer: Model version mismatch model: ${modelVersionString} env: ${process.env.MODEL_VERSION}`
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.hierarchy.domains()
|
||||
const allClasses = this.hierarchy.getDescendants(core.class.Doc)
|
||||
for (const c of allClasses) {
|
||||
if (this.cancelling) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!isClassIndexable(this.hierarchy, c)) {
|
||||
// No need, since no indexable fields or attachments.
|
||||
continue
|
||||
}
|
||||
|
||||
console.log(this.workspace.name, 'checking index', c)
|
||||
|
||||
const generationId = generateId()
|
||||
|
||||
let lastId = ''
|
||||
|
||||
while (true) {
|
||||
this.hierarchy.domains()
|
||||
const allClasses = this.hierarchy.getDescendants(core.class.Doc)
|
||||
for (const c of allClasses) {
|
||||
if (this.cancelling) {
|
||||
return
|
||||
}
|
||||
|
||||
let newDocs: DocIndexState[] = []
|
||||
let updates = new Map<Ref<DocIndexState>, DocumentUpdate<DocIndexState>>()
|
||||
if (!isClassIndexable(this.hierarchy, c)) {
|
||||
// No need, since no indexable fields or attachments.
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const docs = await dbStorage.findAll<Doc>(
|
||||
this.metrics,
|
||||
c,
|
||||
{ _class: c, _id: { $gt: lastId as any } },
|
||||
{
|
||||
limit: 10000,
|
||||
sort: { _id: 1 },
|
||||
projection: { _id: 1, attachedTo: 1, attachedToClass: 1 } as any
|
||||
console.log(this.workspace.name, 'checking index', c)
|
||||
|
||||
const generationId = generateId()
|
||||
|
||||
let lastId = ''
|
||||
|
||||
while (true) {
|
||||
if (this.cancelling) {
|
||||
return
|
||||
}
|
||||
|
||||
let newDocs: DocIndexState[] = []
|
||||
let updates = new Map<Ref<DocIndexState>, DocumentUpdate<DocIndexState>>()
|
||||
|
||||
try {
|
||||
const docs = await dbStorage.findAll<Doc>(
|
||||
ctx,
|
||||
c,
|
||||
{ _class: c, _id: { $gt: lastId as any } },
|
||||
{
|
||||
limit: 10000,
|
||||
sort: { _id: 1 },
|
||||
projection: { _id: 1, attachedTo: 1, attachedToClass: 1 } as any,
|
||||
prefix: 'indexer'
|
||||
}
|
||||
)
|
||||
|
||||
if (docs.length === 0) {
|
||||
// All updated for this class
|
||||
break
|
||||
}
|
||||
)
|
||||
|
||||
if (docs.length === 0) {
|
||||
// All updated for this class
|
||||
lastId = docs[docs.length - 1]._id
|
||||
|
||||
const states = (
|
||||
await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{
|
||||
objectClass: c,
|
||||
_id: {
|
||||
$gte: docs[0]._id as any,
|
||||
$lte: docs[docs.length - 1]._id as any
|
||||
}
|
||||
},
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
).map((it) => it._id)
|
||||
const statesSet = new Set(states)
|
||||
|
||||
// create missing index states
|
||||
newDocs = docs
|
||||
.filter((it) => !statesSet.has(it._id as Ref<DocIndexState>))
|
||||
.map((it) => {
|
||||
return createStateDoc(it._id, c, {
|
||||
generationId,
|
||||
stages: {},
|
||||
attributes: {},
|
||||
removed: false,
|
||||
space: it.space,
|
||||
attachedTo: (it as AttachedDoc)?.attachedTo ?? undefined,
|
||||
attachedToClass: (it as AttachedDoc)?.attachedToClass ?? undefined
|
||||
})
|
||||
})
|
||||
|
||||
// update generationId for existing index states
|
||||
updates = new Map()
|
||||
docs
|
||||
.filter((it) => statesSet.has(it._id as Ref<DocIndexState>))
|
||||
.forEach((it) => {
|
||||
updates.set(it._id as Ref<DocIndexState>, { generationId })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
break
|
||||
}
|
||||
|
||||
lastId = docs[docs.length - 1]._id
|
||||
try {
|
||||
await this.storage.update(DOMAIN_DOC_INDEX_STATE, updates)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
const states = (
|
||||
await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{
|
||||
objectClass: c,
|
||||
_id: {
|
||||
$gte: docs[0]._id as any,
|
||||
$lte: docs[docs.length - 1]._id as any
|
||||
}
|
||||
},
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
).map((it) => it._id)
|
||||
const statesSet = new Set(states)
|
||||
try {
|
||||
await this.storage.upload(DOMAIN_DOC_INDEX_STATE, newDocs)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// create missing index states
|
||||
newDocs = docs
|
||||
.filter((it) => !statesSet.has(it._id as Ref<DocIndexState>))
|
||||
.map((it) => {
|
||||
return createStateDoc(it._id, c, {
|
||||
generationId,
|
||||
stages: {},
|
||||
attributes: {},
|
||||
removed: false,
|
||||
space: it.space,
|
||||
attachedTo: (it as AttachedDoc)?.attachedTo ?? undefined,
|
||||
attachedToClass: (it as AttachedDoc)?.attachedToClass ?? undefined
|
||||
})
|
||||
})
|
||||
// remove index states for documents that do not exist
|
||||
const toRemove = (
|
||||
await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{ objectClass: c, generationId: { $ne: generationId } },
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
).map((it) => it._id)
|
||||
|
||||
// update generationId for existing index states
|
||||
updates = new Map()
|
||||
docs
|
||||
.filter((it) => statesSet.has(it._id as Ref<DocIndexState>))
|
||||
.forEach((it) => {
|
||||
updates.set(it._id as Ref<DocIndexState>, { generationId })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (toRemove.length > 0) {
|
||||
await this.storage.clean(DOMAIN_DOC_INDEX_STATE, toRemove)
|
||||
await this.storage.clean(DOMAIN_FULLTEXT_BLOB, toRemove)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean for non existing classes
|
||||
|
||||
while (true) {
|
||||
const docRefs = await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{ objectClass: { $nin: allClasses } },
|
||||
{ projection: { _id: 1, objectClass: 1 }, limit: 10000 }
|
||||
)
|
||||
const unknownClasses = docRefs.map((it) => it._id)
|
||||
|
||||
console.log('cleaning', docRefs.length, Array.from(new Set(docRefs.map((it) => it.objectClass))).join(', '))
|
||||
|
||||
if (unknownClasses.length > 0) {
|
||||
await this.storage.clean(DOMAIN_DOC_INDEX_STATE, unknownClasses)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
try {
|
||||
await this.storage.update(DOMAIN_DOC_INDEX_STATE, updates)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
try {
|
||||
await this.storage.upload(DOMAIN_DOC_INDEX_STATE, newDocs)
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// remove index states for documents that do not exist
|
||||
const toRemove = (
|
||||
await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{ objectClass: c, generationId: { $ne: generationId } },
|
||||
{ projection: { _id: 1 } }
|
||||
)
|
||||
).map((it) => it._id)
|
||||
|
||||
if (toRemove.length > 0) {
|
||||
await this.storage.clean(DOMAIN_DOC_INDEX_STATE, toRemove)
|
||||
await this.storage.clean(DOMAIN_FULLTEXT_BLOB, toRemove)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean for non existing classes
|
||||
|
||||
while (true) {
|
||||
const docRefs = await this.storage.findAll(
|
||||
core.class.DocIndexState,
|
||||
{ objectClass: { $nin: allClasses } },
|
||||
{ projection: { _id: 1, objectClass: 1 }, limit: 10000 }
|
||||
)
|
||||
const unknownClasses = docRefs.map((it) => it._id)
|
||||
|
||||
console.log('cleaning', docRefs.length, Array.from(new Set(docRefs.map((it) => it.objectClass))).join(', '))
|
||||
|
||||
if (unknownClasses.length > 0) {
|
||||
await this.storage.clean(DOMAIN_DOC_INDEX_STATE, unknownClasses)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+63
-18
@@ -58,7 +58,8 @@ import core, {
|
||||
generateId
|
||||
} from '@hcengineering/core'
|
||||
import { MinioService } from '@hcengineering/minio'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { Metadata, getResource } from '@hcengineering/platform'
|
||||
import { LiveQuery as LQ } from '@hcengineering/query'
|
||||
import crypto from 'node:crypto'
|
||||
import { DbAdapter, DbAdapterConfiguration, TxAdapter } from './adapter'
|
||||
import { createContentAdapter } from './content'
|
||||
@@ -113,6 +114,10 @@ class TServerStorage implements ServerStorage {
|
||||
|
||||
hashes!: string[]
|
||||
|
||||
triggerData = new Map<Metadata<any>, any>()
|
||||
|
||||
liveQuery: LQ
|
||||
|
||||
constructor (
|
||||
private readonly _domains: Record<string, string>,
|
||||
private readonly defaultAdapter: string,
|
||||
@@ -128,6 +133,33 @@ class TServerStorage implements ServerStorage {
|
||||
metrics: MeasureContext,
|
||||
readonly model: Tx[]
|
||||
) {
|
||||
this.liveQuery = new LQ({
|
||||
getHierarchy (): Hierarchy {
|
||||
return hierarchy
|
||||
},
|
||||
getModel (): ModelDb {
|
||||
return modelDb
|
||||
},
|
||||
close: async () => {},
|
||||
findAll: async (_class, query, options) => {
|
||||
return await metrics.with('query', {}, async (ctx) => await this.findAll(ctx, _class, query, options))
|
||||
},
|
||||
findOne: async (_class, query, options) => {
|
||||
return (
|
||||
await metrics.with(
|
||||
'query',
|
||||
{},
|
||||
async (ctx) => await this.findAll(ctx, _class, query, { ...options, limit: 1 })
|
||||
)
|
||||
)[0]
|
||||
},
|
||||
tx: async (tx) => {
|
||||
return {}
|
||||
},
|
||||
searchFulltext: async (query: SearchQuery, options: SearchOptions) => {
|
||||
return await metrics.with('query', {}, async (ctx) => await this.searchFulltext(ctx, query, options))
|
||||
}
|
||||
})
|
||||
this.hierarchy = hierarchy
|
||||
this.fulltext = indexFactory(this)
|
||||
|
||||
@@ -165,16 +197,24 @@ class TServerStorage implements ServerStorage {
|
||||
const adapter = this.getAdapter(lastDomain as Domain)
|
||||
const toDelete = part.filter((it) => it._class === core.class.TxRemoveDoc).map((it) => it.objectId)
|
||||
|
||||
const toDeleteDocs = await ctx.with(
|
||||
'adapter-load',
|
||||
{ domain: lastDomain },
|
||||
async () => await adapter.load(lastDomain as Domain, toDelete)
|
||||
)
|
||||
for (const ddoc of toDeleteDocs) {
|
||||
removedDocs.set(ddoc._id, ddoc)
|
||||
if (toDelete.length > 0) {
|
||||
const toDeleteDocs = await ctx.with(
|
||||
'adapter-load',
|
||||
{ domain: lastDomain },
|
||||
async () => await adapter.load(lastDomain as Domain, toDelete)
|
||||
)
|
||||
|
||||
for (const ddoc of toDeleteDocs) {
|
||||
removedDocs.set(ddoc._id, ddoc)
|
||||
}
|
||||
}
|
||||
|
||||
const r = await ctx.with('adapter-tx', {}, async () => await adapter.tx(...part))
|
||||
const r = await ctx.with('adapter-tx', { domain: lastDomain }, async () => await adapter.tx(...part))
|
||||
|
||||
// Update server live queries.
|
||||
for (const t of part) {
|
||||
await this.liveQuery.tx(t)
|
||||
}
|
||||
if (Array.isArray(r)) {
|
||||
result.push(...r)
|
||||
} else {
|
||||
@@ -374,14 +414,19 @@ class TServerStorage implements ServerStorage {
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T> & {
|
||||
domain?: Domain // Allow to find for Doc's in specified domain only.
|
||||
prefix?: string
|
||||
}
|
||||
): Promise<FindResult<T>> {
|
||||
const p = options?.prefix ?? 'client'
|
||||
const domain = options?.domain ?? this.hierarchy.getDomain(clazz)
|
||||
if (query?.$search !== undefined) {
|
||||
return await ctx.with('client-fulltext-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options))
|
||||
return await ctx.with(p + '-fulltext-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options))
|
||||
}
|
||||
return await ctx.with('client-find-all', { _class: clazz }, () =>
|
||||
this.getAdapter(domain).findAll(clazz, query, options)
|
||||
return await ctx.with(
|
||||
p + '-find-all',
|
||||
{ _class: clazz },
|
||||
() => this.getAdapter(domain).findAll(clazz, query, options),
|
||||
{ clazz, query, options }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -586,6 +631,10 @@ class TServerStorage implements ServerStorage {
|
||||
},
|
||||
applyCtx: async (ctx, tx, broadcast) => {
|
||||
return await this.apply(ctx, tx, broadcast)
|
||||
},
|
||||
// Will create a live query if missing and return values immediately if already asked.
|
||||
queryFind: async (_class, query, options) => {
|
||||
return await this.liveQuery.queryFind(_class, query, options)
|
||||
}
|
||||
}
|
||||
const triggers = await ctx.with('process-triggers', {}, async (ctx) => {
|
||||
@@ -707,11 +756,7 @@ class TServerStorage implements ServerStorage {
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> => {
|
||||
const domain = this.hierarchy.getDomain(clazz)
|
||||
if (query?.$search !== undefined) {
|
||||
return await ctx.with('full-text-find-all', {}, (ctx) => this.fulltext.findAll(ctx, clazz, query, options))
|
||||
}
|
||||
return await ctx.with('find-all', { _class: clazz }, () => this.getAdapter(domain).findAll(clazz, query, options))
|
||||
return await this.findAll(ctx, clazz, query, { ...options, prefix: 'server' })
|
||||
}
|
||||
const txToStore: Tx[] = []
|
||||
const modelTx: Tx[] = []
|
||||
@@ -751,7 +796,7 @@ class TServerStorage implements ServerStorage {
|
||||
|
||||
// index object
|
||||
for (const _tx of txToProcess) {
|
||||
await ctx.with('fulltext', {}, (ctx) => this.fulltext.tx(ctx, _tx))
|
||||
await ctx.with('fulltext-tx', {}, (ctx) => this.fulltext.tx(ctx, _tx))
|
||||
}
|
||||
|
||||
// index derived objects
|
||||
|
||||
@@ -128,9 +128,6 @@ export interface TriggerControl {
|
||||
modelDb: ModelDb
|
||||
removedMap: Map<Ref<Doc>, Doc>
|
||||
|
||||
// // An object cache,
|
||||
// getCachedObject: <T extends Doc>(_class: Ref<Class<T>>, _id: Ref<T>) => Promise<T | undefined>
|
||||
|
||||
fulltextFx: (f: (adapter: FullTextAdapter) => Promise<void>) => void
|
||||
// Since we don't have other storages let's consider adapter is MinioClient
|
||||
// Later can be replaced with generic one with bucket encapsulated inside.
|
||||
@@ -140,6 +137,13 @@ export interface TriggerControl {
|
||||
// Bulk operations in case trigger require some
|
||||
apply: (tx: Tx[], broadcast: boolean) => Promise<TxResult>
|
||||
applyCtx: (ctx: MeasureContext, tx: Tx[], broadcast: boolean) => Promise<TxResult>
|
||||
|
||||
// Will create a live query if missing and return values immediately if already asked.
|
||||
queryFind: <T extends Doc>(
|
||||
_class: Ref<Class<T>>,
|
||||
query: DocumentQuery<T>,
|
||||
options?: FindOptions<T>
|
||||
) => Promise<FindResult<T>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -283,7 +287,7 @@ export class DummyFullTextAdapter implements FullTextAdapter {
|
||||
async close (): Promise<void> {}
|
||||
|
||||
metrics (): MeasureContext {
|
||||
return new MeasureMetricsContext('', {})
|
||||
return new MeasureMetricsContext('', {}, {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export function getMetricsContext (): MeasureContext {
|
||||
console.info('please provide apm server url for monitoring')
|
||||
|
||||
const metrics = newMetrics()
|
||||
metricsContext = new MeasureMetricsContext('System', {}, metrics)
|
||||
metricsContext = new MeasureMetricsContext('System', {}, {}, metrics)
|
||||
|
||||
if (metricsFile !== undefined || metricsConsole) {
|
||||
console.info('storing measurements into local file', metricsFile)
|
||||
|
||||
Reference in New Issue
Block a user