mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Do not create Github worker for deleting workspace (#10661)
Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import type { MeasureContext, WorkspaceInfoWithStatus, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { GithubWorkerWorkspaceState, getGithubWorkerState } from '../workspaceUtils'
|
||||
|
||||
const WS = '00000000-0000-0000-0000-000000000001' as WorkspaceUuid
|
||||
|
||||
const DAY_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
function createMockCtx (): MeasureContext {
|
||||
return {
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn()
|
||||
} as unknown as MeasureContext
|
||||
}
|
||||
|
||||
function baseInfo (overrides: Partial<WorkspaceInfoWithStatus> = {}): WorkspaceInfoWithStatus {
|
||||
return {
|
||||
uuid: WS,
|
||||
name: 'ws',
|
||||
url: 'ws-url',
|
||||
createdOn: 0,
|
||||
versionMajor: 0,
|
||||
versionMinor: 7,
|
||||
versionPatch: 0,
|
||||
mode: 'active',
|
||||
processingAttemps: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('getGithubWorkerState', () => {
|
||||
it('returns Skip and warns when uuid is absent', () => {
|
||||
const ctx = createMockCtx()
|
||||
const checked = new Set<string>()
|
||||
const info = { ...baseInfo(), uuid: undefined as unknown as WorkspaceUuid }
|
||||
const state = getGithubWorkerState(ctx, WS, info, 3, checked)
|
||||
expect(state).toBe(GithubWorkerWorkspaceState.Skip)
|
||||
expect(ctx.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns Skip when workspace is disabled', () => {
|
||||
const ctx = createMockCtx()
|
||||
const state = getGithubWorkerState(ctx, WS, baseInfo({ isDisabled: true, mode: 'active' }), 3, new Set())
|
||||
expect(state).toBe(GithubWorkerWorkspaceState.Skip)
|
||||
expect(ctx.info).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['pending-deletion', 'deleting', 'deleted'] as const)('returns Skip for mode %s', (mode) => {
|
||||
const ctx = createMockCtx()
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode }), 3, new Set())).toBe(GithubWorkerWorkspaceState.Skip)
|
||||
})
|
||||
|
||||
it.each(['archived', 'archiving-pending-backup', 'archiving-clean'] as const)(
|
||||
'returns Skip for archiving %s',
|
||||
(mode) => {
|
||||
const ctx = createMockCtx()
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode }), 3, new Set())).toBe(GithubWorkerWorkspaceState.Skip)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['upgrading', 'creating', 'pending-creation'] as const)('returns Wait for mode %s', (mode) => {
|
||||
const ctx = createMockCtx()
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode }), 3, new Set())).toBe(GithubWorkerWorkspaceState.Wait)
|
||||
expect(ctx.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns Wait when last visit exceeds inactivity interval', () => {
|
||||
const ctx = createMockCtx()
|
||||
const nowMs = 1_700_000_000_000
|
||||
const lastVisit = nowMs - 4 * DAY_MS
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode: 'active', lastVisit }), 3, new Set(), nowMs)).toBe(
|
||||
GithubWorkerWorkspaceState.Wait
|
||||
)
|
||||
})
|
||||
|
||||
it('returns Connect when within inactivity interval', () => {
|
||||
const ctx = createMockCtx()
|
||||
const nowMs = 1_700_000_000_000
|
||||
const lastVisit = nowMs - 2 * DAY_MS
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode: 'active', lastVisit }), 3, new Set(), nowMs)).toBe(
|
||||
GithubWorkerWorkspaceState.Connect
|
||||
)
|
||||
})
|
||||
|
||||
it('does not apply inactivity gate when interval is 0', () => {
|
||||
const ctx = createMockCtx()
|
||||
const nowMs = 1_700_000_000_000
|
||||
const lastVisit = nowMs - 365 * DAY_MS
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode: 'active', lastVisit }), 0, new Set(), nowMs)).toBe(
|
||||
GithubWorkerWorkspaceState.Connect
|
||||
)
|
||||
})
|
||||
|
||||
it('missing lastVisit uses epoch → Wait inactive when interval > 0', () => {
|
||||
const ctx = createMockCtx()
|
||||
const nowMs = 1_700_000_000_000
|
||||
expect(getGithubWorkerState(ctx, WS, baseInfo({ mode: 'active' }), 3, new Set(), nowMs)).toBe(
|
||||
GithubWorkerWorkspaceState.Wait
|
||||
)
|
||||
})
|
||||
|
||||
it('logs inactive warning only once per workspace id', () => {
|
||||
const ctx = createMockCtx()
|
||||
const checked = new Set<string>()
|
||||
const nowMs = 1_700_000_000_000
|
||||
const info = baseInfo({ mode: 'active', lastVisit: nowMs - 10 * DAY_MS })
|
||||
getGithubWorkerState(ctx, WS, info, 3, checked, nowMs)
|
||||
getGithubWorkerState(ctx, WS, info, 3, checked, nowMs)
|
||||
expect(ctx.warn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -11,8 +11,6 @@ import core, {
|
||||
Client,
|
||||
ClientConnectEvent,
|
||||
DocumentUpdate,
|
||||
isActiveMode,
|
||||
isDeletingMode,
|
||||
MeasureContext,
|
||||
PersonId,
|
||||
RateLimiter,
|
||||
@@ -46,6 +44,7 @@ import { type StorageAdapter } from '@hcengineering/server-core'
|
||||
import { join } from 'path'
|
||||
import { createPlatformClient } from './client'
|
||||
import config from './config'
|
||||
import { GithubWorkerWorkspaceState, getGithubWorkerState } from './workspaceUtils'
|
||||
import { registerLoaders } from './loaders'
|
||||
import { createNotification } from './notifications'
|
||||
import { errorToObj } from './sync/utils'
|
||||
@@ -68,6 +67,8 @@ interface IntegrationDataValue {
|
||||
installationId: number | number[]
|
||||
}
|
||||
|
||||
export { GithubWorkerWorkspaceState, getGithubWorkerState } from './workspaceUtils'
|
||||
|
||||
export class PlatformWorker {
|
||||
private readonly clients = new Map<WorkspaceUuid, GithubWorker>()
|
||||
|
||||
@@ -948,32 +949,6 @@ export class PlatformWorker {
|
||||
|
||||
checkedWorkspaces = new Set<string>()
|
||||
|
||||
checkWorkspaceIsActive (workspace: WorkspaceUuid, workspaceInfo: WorkspaceInfoWithStatus): boolean {
|
||||
if (workspaceInfo?.uuid === undefined) {
|
||||
this.ctx.error('No workspace exists for workspaceId', { workspace })
|
||||
return false
|
||||
}
|
||||
if (workspaceInfo?.isDisabled === true || isDeletingMode(workspaceInfo?.mode)) {
|
||||
this.ctx.warn('Workspace is disabled', { workspace })
|
||||
return false
|
||||
}
|
||||
if (!isActiveMode(workspaceInfo?.mode)) {
|
||||
this.ctx.warn('Workspace is in maitenance, skipping for now.', { workspace, mode: workspaceInfo?.mode })
|
||||
return true
|
||||
}
|
||||
|
||||
const lastVisit = (Date.now() - (workspaceInfo.lastVisit ?? 0)) / (3600 * 24 * 1000) // In days
|
||||
|
||||
if (config.WorkspaceInactivityInterval > 0 && lastVisit > config.WorkspaceInactivityInterval) {
|
||||
if (!this.checkedWorkspaces.has(workspace)) {
|
||||
this.checkedWorkspaces.add(workspace)
|
||||
this.ctx.warn('Workspace is inactive for too long, skipping for now.', { workspace })
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
checkReconnect (workspace: WorkspaceUuid, event: ClientConnectEvent, worker: GithubWorker): void {
|
||||
if (event === ClientConnectEvent.Refresh || event === ClientConnectEvent.Upgraded) {
|
||||
void this.clients
|
||||
@@ -989,9 +964,15 @@ export class PlatformWorker {
|
||||
getAccountClient(config.AccountsURL, token)
|
||||
.getWorkspaceInfo()
|
||||
.then((wsInfo) => {
|
||||
const res = this.checkWorkspaceIsActive(workspace, wsInfo)
|
||||
if (!res) {
|
||||
this.ctx.warn('Workspace is inactive, removing from clients list.', { workspace })
|
||||
const state = getGithubWorkerState(
|
||||
this.ctx,
|
||||
workspace,
|
||||
wsInfo,
|
||||
config.WorkspaceInactivityInterval,
|
||||
this.checkedWorkspaces
|
||||
)
|
||||
if (state === GithubWorkerWorkspaceState.Skip) {
|
||||
this.ctx.warn('Github worker state is skip, removing from clients list.', { workspace })
|
||||
this.clients.delete(workspace)
|
||||
void worker?.close().catch((err) => {
|
||||
this.ctx.error('Failed to close workspace', { workspace, error: err })
|
||||
@@ -1053,11 +1034,20 @@ export class PlatformWorker {
|
||||
rechecks.push(workspace)
|
||||
continue
|
||||
}
|
||||
const needRecheck = this.checkWorkspaceIsActive(workspace, returnedInfo)
|
||||
if (needRecheck) {
|
||||
const state = getGithubWorkerState(
|
||||
this.ctx,
|
||||
workspace,
|
||||
returnedInfo,
|
||||
config.WorkspaceInactivityInterval,
|
||||
this.checkedWorkspaces
|
||||
)
|
||||
if (state === GithubWorkerWorkspaceState.Wait) {
|
||||
rechecks.push(workspace)
|
||||
continue
|
||||
}
|
||||
if (state === GithubWorkerWorkspaceState.Skip) {
|
||||
continue
|
||||
}
|
||||
await rateLimiter.add(async () => {
|
||||
try {
|
||||
const branding = Object.values(this.brandingMap).find((b) => b.key === returnedInfo?.branding) ?? null
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
//
|
||||
// Copyright © 2026 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import {
|
||||
isActiveMode,
|
||||
isArchivingMode,
|
||||
isDeletingMode,
|
||||
type MeasureContext,
|
||||
type WorkspaceInfoWithStatus,
|
||||
type WorkspaceUuid
|
||||
} from '@hcengineering/core'
|
||||
|
||||
/** Whether this pod should run a GithubWorker for the workspace. */
|
||||
export enum GithubWorkerWorkspaceState {
|
||||
/** Active workspace — connect and run worker. */
|
||||
Connect = 'connect',
|
||||
/** Not ready yet (upgrade, creation, …) or too inactive — recheck later; keep existing worker. */
|
||||
Wait = 'wait',
|
||||
/** Disabled, deleting, deleted, or archiving — do not run; drop worker if present. */
|
||||
Skip = 'skip'
|
||||
}
|
||||
|
||||
export function getGithubWorkerState (
|
||||
ctx: MeasureContext,
|
||||
workspace: WorkspaceUuid,
|
||||
workspaceInfo: WorkspaceInfoWithStatus,
|
||||
inactivityIntervalDays: number,
|
||||
checkedWorkspaces: Set<string>,
|
||||
nowMs: number = Date.now()
|
||||
): GithubWorkerWorkspaceState {
|
||||
if (workspaceInfo?.uuid === undefined) {
|
||||
ctx.warn('No workspace exists for workspaceId', { workspace })
|
||||
return GithubWorkerWorkspaceState.Skip
|
||||
}
|
||||
if (workspaceInfo.isDisabled === true || isDeletingMode(workspaceInfo.mode) || isArchivingMode(workspaceInfo.mode)) {
|
||||
ctx.info('Workspace is disabled, deleting, or archived — skipping github worker', {
|
||||
workspace,
|
||||
mode: workspaceInfo.mode,
|
||||
isDisabled: workspaceInfo.isDisabled
|
||||
})
|
||||
return GithubWorkerWorkspaceState.Skip
|
||||
}
|
||||
if (!isActiveMode(workspaceInfo.mode)) {
|
||||
ctx.warn('Workspace is in maintenance, skipping for now.', { workspace, mode: workspaceInfo.mode })
|
||||
return GithubWorkerWorkspaceState.Wait
|
||||
}
|
||||
|
||||
const lastVisitDays = (nowMs - (workspaceInfo.lastVisit ?? 0)) / (3600 * 24 * 1000)
|
||||
|
||||
if (inactivityIntervalDays > 0 && lastVisitDays > inactivityIntervalDays) {
|
||||
if (!checkedWorkspaces.has(workspace)) {
|
||||
checkedWorkspaces.add(workspace)
|
||||
ctx.warn('Workspace is inactive for too long, skipping for now.', { workspace })
|
||||
}
|
||||
return GithubWorkerWorkspaceState.Wait
|
||||
}
|
||||
return GithubWorkerWorkspaceState.Connect
|
||||
}
|
||||
Reference in New Issue
Block a user