UBERF-8899: Reconnect performance issues (#7611)
Some checks are pending
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2025-01-09 11:15:06 +07:00 committed by GitHub
parent d63db0fca6
commit 12e0aaa5f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 73 additions and 52 deletions

View File

@ -329,6 +329,7 @@ async function tryLoadModel (
if (conn.getLastHash !== undefined && (await conn.getLastHash(ctx)) === current.hash) { if (conn.getLastHash !== undefined && (await conn.getLastHash(ctx)) === current.hash) {
// We have same model hash. // We have same model hash.
current.full = false // Since we load, no need to send full
return current return current
} }
const lastTxTime = getLastTxTime(current.transactions) const lastTxTime = getLastTxTime(current.transactions)

View File

@ -44,6 +44,7 @@ import core, {
TxApplyIf, TxApplyIf,
TxHandler, TxHandler,
TxResult, TxResult,
clone,
generateId, generateId,
toFindResult, toFindResult,
type MeasureContext type MeasureContext
@ -108,6 +109,8 @@ class Connection implements ClientConnection {
private helloRecieved: boolean = false private helloRecieved: boolean = false
private account: Account | undefined
onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void> onConnect?: (event: ClientConnectEvent, lastTx: string | undefined, data: any) => Promise<void>
rpcHandler = new RPCHandler() rpcHandler = new RPCHandler()
@ -303,7 +306,7 @@ class Connection implements ClientConnection {
this.websocket?.close() this.websocket?.close()
return return
} }
this.account = helloResp.account
this.helloRecieved = true this.helloRecieved = true
if (this.upgrading) { if (this.upgrading) {
// We need to call upgrade since connection is upgraded // We need to call upgrade since connection is upgraded
@ -322,8 +325,8 @@ class Connection implements ClientConnection {
} }
void this.onConnect?.( void this.onConnect?.(
(resp as HelloResponse).reconnect === true ? ClientConnectEvent.Reconnected : ClientConnectEvent.Connected, helloResp.reconnect === true ? ClientConnectEvent.Reconnected : ClientConnectEvent.Connected,
(resp as HelloResponse).lastTx, helloResp.lastTx,
this.sessionId this.sessionId
) )
this.schedulePing(socketId) this.schedulePing(socketId)
@ -635,6 +638,9 @@ class Connection implements ClientConnection {
} }
getAccount (): Promise<Account> { getAccount (): Promise<Account> {
if (this.account !== undefined) {
return clone(this.account)
}
return this.sendRequest({ method: 'getAccount', params: [] }) return this.sendRequest({ method: 'getAccount', params: [] })
} }

View File

@ -227,7 +227,7 @@ export async function connect (title: string): Promise<Client | undefined> {
return return
} }
try { try {
if (event === ClientConnectEvent.Connected) { if (event === ClientConnectEvent.Connected || event === ClientConnectEvent.Reconnected) {
setMetadata(presentation.metadata.SessionId, data) setMetadata(presentation.metadata.SessionId, data)
} }
if ((_clientSet && event === ClientConnectEvent.Connected) || event === ClientConnectEvent.Refresh) { if ((_clientSet && event === ClientConnectEvent.Connected) || event === ClientConnectEvent.Refresh) {

View File

@ -64,43 +64,47 @@ export class DbAdapterManagerImpl implements DBAdapterManager {
return this.defaultAdapter return this.defaultAdapter
} }
async registerHelper (helper: DomainHelper): Promise<void> { async registerHelper (ctx: MeasureContext, helper: DomainHelper): Promise<void> {
this.domainHelper = helper this.domainHelper = helper
await this.initDomains() await this.initDomains(ctx)
} }
async initDomains (): Promise<void> { async initDomains (ctx: MeasureContext): Promise<void> {
const adapterDomains = new Map<DbAdapter, Set<Domain>>() const adapterDomains = new Map<DbAdapter, Set<Domain>>()
for (const d of this.context.hierarchy.domains()) { for (const d of this.context.hierarchy.domains()) {
// We need to init domain info // We need to init domain info
const info = this.getDomainInfo(d) await ctx.with('update-info', { domain: d }, async (ctx) => {
await this.updateInfo(d, adapterDomains, info) const info = this.getDomainInfo(d)
await this.updateInfo(d, adapterDomains, info)
})
} }
for (const adapter of this.adapters.values()) { for (const [name, adapter] of this.adapters.entries()) {
adapter.on?.((domain, event, count, helper) => { await ctx.with('domain-helper', { name }, async (ctx) => {
const info = this.getDomainInfo(domain) adapter.on?.((domain, event, count, helper) => {
const oldDocuments = info.documents const info = this.getDomainInfo(domain)
switch (event) { const oldDocuments = info.documents
case 'add': switch (event) {
info.documents += count case 'add':
break info.documents += count
case 'update': break
break case 'update':
case 'delete': break
info.documents -= count case 'delete':
break info.documents -= count
case 'read': break
break case 'read':
} break
}
if (oldDocuments < 50 && info.documents > 50) { if (oldDocuments < 50 && info.documents > 50) {
// We have more 50 documents, we need to check for indexes // We have more 50 documents, we need to check for indexes
void this.domainHelper?.checkDomain(this.metrics, domain, info.documents, helper) void this.domainHelper?.checkDomain(this.metrics, domain, info.documents, helper)
} }
if (oldDocuments > 50 && info.documents < 50) { if (oldDocuments > 50 && info.documents < 50) {
// We have more 50 documents, we need to check for indexes // We have more 50 documents, we need to check for indexes
void this.domainHelper?.checkDomain(this.metrics, domain, info.documents, helper) void this.domainHelper?.checkDomain(this.metrics, domain, info.documents, helper)
} }
})
}) })
} }
} }

View File

@ -16,6 +16,7 @@
import { Analytics } from '@hcengineering/analytics' import { Analytics } from '@hcengineering/analytics'
import { import {
toFindResult, toFindResult,
withContext,
type Class, type Class,
type Doc, type Doc,
type DocumentQuery, type DocumentQuery,
@ -66,6 +67,7 @@ class PipelineImpl implements Pipeline {
return pipeline return pipeline
} }
@withContext('build-chain')
private async buildChain ( private async buildChain (
ctx: MeasureContext, ctx: MeasureContext,
constructors: MiddlewareCreator[], constructors: MiddlewareCreator[],

View File

@ -151,7 +151,7 @@ export interface DBAdapterManager {
close: () => Promise<void> close: () => Promise<void>
registerHelper: (helper: DomainHelper) => Promise<void> registerHelper: (ctx: MeasureContext, helper: DomainHelper) => Promise<void>
initAdapters: (ctx: MeasureContext) => Promise<void> initAdapters: (ctx: MeasureContext) => Promise<void>

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import { type MeasureContext } from '@hcengineering/core' import { withContext, type MeasureContext } from '@hcengineering/core'
import type { Middleware, PipelineContext } from '@hcengineering/server-core' import type { Middleware, PipelineContext } from '@hcengineering/server-core'
import { BaseMiddleware, DomainIndexHelperImpl } from '@hcengineering/server-core' import { BaseMiddleware, DomainIndexHelperImpl } from '@hcengineering/server-core'
@ -21,14 +21,19 @@ import { BaseMiddleware, DomainIndexHelperImpl } from '@hcengineering/server-cor
* @public * @public
*/ */
export class DBAdapterInitMiddleware extends BaseMiddleware implements Middleware { export class DBAdapterInitMiddleware extends BaseMiddleware implements Middleware {
@withContext('db-adapter-init')
static async create ( static async create (
ctx: MeasureContext, ctx: MeasureContext,
context: PipelineContext, context: PipelineContext,
next?: Middleware next?: Middleware
): Promise<Middleware | undefined> { ): Promise<Middleware | undefined> {
await context.adapterManager?.initAdapters?.(ctx) await ctx.with('init-adapters', {}, async (ctx) => {
await context.adapterManager?.initAdapters?.(ctx)
})
const domainHelper = new DomainIndexHelperImpl(ctx, context.hierarchy, context.modelDb, context.workspace) const domainHelper = new DomainIndexHelperImpl(ctx, context.hierarchy, context.modelDb, context.workspace)
await context.adapterManager?.registerHelper?.(domainHelper) await ctx.with('register-helper', {}, async (ctx) => {
await context.adapterManager?.registerHelper?.(ctx, domainHelper)
})
return undefined return undefined
} }
} }

View File

@ -17,6 +17,7 @@ import core, {
Domain, Domain,
groupByArray, groupByArray,
TxProcessor, TxProcessor,
withContext,
type Doc, type Doc,
type MeasureContext, type MeasureContext,
type SessionData, type SessionData,
@ -41,6 +42,7 @@ import { BaseMiddleware } from '@hcengineering/server-core'
export class DomainTxMiddleware extends BaseMiddleware implements Middleware { export class DomainTxMiddleware extends BaseMiddleware implements Middleware {
adapterManager!: DBAdapterManager adapterManager!: DBAdapterManager
@withContext('domainTx-middleware')
static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise<Middleware> { static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise<Middleware> {
const middleware = new DomainTxMiddleware(context, next) const middleware = new DomainTxMiddleware(context, next)
if (context.adapterManager == null) { if (context.adapterManager == null) {

View File

@ -20,7 +20,8 @@ import core, {
type Timestamp, type Timestamp,
type Tx, type Tx,
type TxCUD, type TxCUD,
DOMAIN_TX DOMAIN_TX,
withContext
} from '@hcengineering/core' } from '@hcengineering/core'
import { PlatformError, unknownError } from '@hcengineering/platform' import { PlatformError, unknownError } from '@hcengineering/platform'
import type { import type {
@ -51,6 +52,7 @@ export class ModelMiddleware extends BaseMiddleware implements Middleware {
super(context, next) super(context, next)
} }
@withContext('modelAdapter-middleware')
static async doCreate ( static async doCreate (
ctx: MeasureContext, ctx: MeasureContext,
context: PipelineContext, context: PipelineContext,

View File

@ -450,12 +450,8 @@ export class DBCollectionHelper implements DomainHelperOperations {
async create (domain: Domain): Promise<void> {} async create (domain: Domain): Promise<void> {}
async exists (domain: Domain): Promise<boolean> { async exists (domain: Domain): Promise<boolean> {
const exists = await this.client` // Always exists. We don't need to check for index existence
SELECT table_name return true
FROM information_schema.tables
WHERE table_name = '${this.client(translateDomain(domain))}'
`
return exists.length > 0
} }
async listDomains (): Promise<Set<Domain>> { async listDomains (): Promise<Set<Domain>> {
@ -469,10 +465,8 @@ export class DBCollectionHelper implements DomainHelperOperations {
} }
async estimatedCount (domain: Domain): Promise<number> { async estimatedCount (domain: Domain): Promise<number> {
const res = await this // We should always return 0, since no controlled index stuff is required for postgres driver
.client`SELECT COUNT(_id) FROM ${this.client(translateDomain(domain))} WHERE "workspaceId" = ${this.workspaceId.name}` return 0
return res.count
} }
} }

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Account } from '@hcengineering/core'
import platform, { PlatformError, Severity, Status } from '@hcengineering/platform' import platform, { PlatformError, Severity, Status } from '@hcengineering/platform'
import { Packr } from 'msgpackr' import { Packr } from 'msgpackr'
@ -48,6 +49,7 @@ export interface HelloResponse extends Response<any> {
serverVersion: string serverVersion: string
lastTx?: string lastTx?: string
lastHash?: string // Last model hash lastHash?: string // Last model hash
account: Account
} }
function replacer (key: string, value: any): any { function replacer (key: string, value: any): any {

View File

@ -418,6 +418,7 @@ class TSessionManager implements SessionManager {
}) })
workspace = this.createWorkspace( workspace = this.createWorkspace(
ctx.parent ?? ctx, ctx.parent ?? ctx,
ctx,
pipelineFactory, pipelineFactory,
token, token,
workspaceInfo.workspaceUrl ?? workspaceInfo.workspaceId, workspaceInfo.workspaceUrl ?? workspaceInfo.workspaceId,
@ -435,7 +436,7 @@ class TSessionManager implements SessionManager {
workspace: workspaceInfo.workspaceId, workspace: workspaceInfo.workspaceId,
wsUrl: workspaceInfo.workspaceUrl wsUrl: workspaceInfo.workspaceUrl
}) })
pipeline = await ctx.with('💤 wait', { workspaceName }, () => (workspace as Workspace).pipeline) pipeline = await ctx.with('💤 wait-pipeline', {}, () => (workspace as Workspace).pipeline)
} else { } else {
ctx.warn('reconnect workspace in upgrade switch', { ctx.warn('reconnect workspace in upgrade switch', {
email: token.email, email: token.email,
@ -466,9 +467,10 @@ class TSessionManager implements SessionManager {
}) })
return { upgrade: true } return { upgrade: true }
} }
try { try {
if (workspace.pipeline instanceof Promise) { if (workspace.pipeline instanceof Promise) {
pipeline = await workspace.pipeline pipeline = await ctx.with('💤 wait-pipeline', {}, () => (workspace as Workspace).pipeline)
workspace.pipeline = pipeline workspace.pipeline = pipeline
} else { } else {
pipeline = workspace.pipeline pipeline = workspace.pipeline
@ -645,6 +647,7 @@ class TSessionManager implements SessionManager {
private createWorkspace ( private createWorkspace (
ctx: MeasureContext, ctx: MeasureContext,
pipelineCtx: MeasureContext,
pipelineFactory: PipelineFactory, pipelineFactory: PipelineFactory,
token: Token, token: Token,
workspaceUrl: string, workspaceUrl: string,
@ -655,7 +658,6 @@ class TSessionManager implements SessionManager {
const wsId = toWorkspaceString(token.workspace) const wsId = toWorkspaceString(token.workspace)
const upgrade = token.extra?.model === 'upgrade' const upgrade = token.extra?.model === 'upgrade'
const context = ctx.newChild('🧲 session', {}) const context = ctx.newChild('🧲 session', {})
const pipelineCtx = context.newChild('🧲 pipeline-factory', {})
const workspace: Workspace = { const workspace: Workspace = {
context, context,
id: generateId(), id: generateId(),
@ -1106,7 +1108,8 @@ class TSessionManager implements SessionManager {
reconnect, reconnect,
serverVersion: this.serverVersion, serverVersion: this.serverVersion,
lastTx: pipeline.context.lastTx, lastTx: pipeline.context.lastTx,
lastHash: pipeline.context.lastHash lastHash: pipeline.context.lastHash,
account: service.getRawAccount(pipeline)
} }
ws.send(requestCtx, helloResponse, false, false) ws.send(requestCtx, helloResponse, false, false)
} }