mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-13 00:03:00 +00:00
fix: ask a workspace for its session backups before pushing to it (#11641)
This commit is contained in:
@@ -998,9 +998,23 @@ async function flush(): Promise<void> {
|
||||
let settledAny = false
|
||||
let leftForNext = false
|
||||
for (const [ws, w] of work) {
|
||||
const out = await pushWorkspace(ws, w, email)
|
||||
if (out.status === 'abort') return
|
||||
if (out.status === 'off') {
|
||||
// A push carries whole transcripts, and a workspace that keeps no backups is
|
||||
// asked again on every page load: one this page has not heard from yet is asked
|
||||
// with the listing first, which costs nothing to refuse.
|
||||
let probe: 'on' | 'off' | 'failed' = 'on'
|
||||
if (!wsState.has(ws)) {
|
||||
const listing = await fetchListing(ws)
|
||||
if (getCurrentUserEmail() !== email) return
|
||||
probe = listing === 'off' || listing === 'failed' ? listing : 'on'
|
||||
if (probe === 'on') wsState.set(ws, 'on')
|
||||
}
|
||||
if (probe === 'failed') {
|
||||
backOff()
|
||||
continue
|
||||
}
|
||||
const out = probe === 'off' ? undefined : await pushWorkspace(ws, w, email)
|
||||
if (out?.status === 'abort') return
|
||||
if (!out || out.status === 'off') {
|
||||
markOff(ws)
|
||||
await staleWorkspaceSync(ws, email)
|
||||
// Same rule as the loop above: a removal is worth keeping only for a session
|
||||
@@ -1189,22 +1203,28 @@ function unpackBackup(
|
||||
|
||||
type BackupListing = Awaited<ReturnType<typeof AiService.listAiSessionBackups>>
|
||||
|
||||
/** The workspace's listing; `off` when it keeps no backups, `failed` when it could not be
|
||||
* listed this time. */
|
||||
async function listWorkspace(ws: string, email: string): Promise<BackupListing | 'off' | 'failed'> {
|
||||
/** The workspace's listing as the server answers it; `off` when it keeps no backups,
|
||||
* `failed` when it could not be listed this time. */
|
||||
async function fetchListing(ws: string): Promise<BackupListing | 'off' | 'failed'> {
|
||||
let listing
|
||||
try {
|
||||
listing = await AiService.listAiSessionBackups({ workspace: ws })
|
||||
} catch (e) {
|
||||
const status = statusOf(e)
|
||||
if (status === 404 || status === 403) {
|
||||
markOff(ws)
|
||||
return 'off'
|
||||
}
|
||||
// 404: a build without object storage, or a storage whose resource is gone.
|
||||
// 403: nothing this token may back up.
|
||||
if (status === 404 || status === 403) return 'off'
|
||||
console.warn('Could not list session backups', e)
|
||||
return 'failed'
|
||||
}
|
||||
if (!listing.enabled) {
|
||||
return listing.enabled ? listing : 'off'
|
||||
}
|
||||
|
||||
/** `fetchListing`, with what it says of the workspace recorded for the page. */
|
||||
async function listWorkspace(ws: string, email: string): Promise<BackupListing | 'off' | 'failed'> {
|
||||
const listing = await fetchListing(ws)
|
||||
if (listing === 'failed') return 'failed'
|
||||
if (listing === 'off') {
|
||||
markOff(ws)
|
||||
return 'off'
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ beforeEach(async () => {
|
||||
__resetMirrorForTesting()
|
||||
pushMock.mockReset()
|
||||
listMock.mockReset()
|
||||
listMock.mockResolvedValue({ enabled: true, sessions: [] })
|
||||
pullMock.mockReset()
|
||||
chatImport.unavailable = false
|
||||
chatImport.pruneFails = false
|
||||
@@ -316,6 +317,30 @@ describe('sessionMirror flush', () => {
|
||||
expect((await __syncRowsForTesting(EMAIL)).some((r) => r.id === 'sr')).toBe(false)
|
||||
})
|
||||
|
||||
it('asks a workspace it has not heard from before sending it a transcript', async () => {
|
||||
const s: Session = { id: 'sp', name: 'session-1', createdAt: 1, workspace_id: 'ws' }
|
||||
sessionState.sessions = [s]
|
||||
await putSession(s)
|
||||
const { ApiError } = await import('$lib/gen')
|
||||
listMock.mockRejectedValueOnce(
|
||||
new ApiError({ method: 'GET', url: '' } as never, { status: 404 } as never, 'no resource')
|
||||
)
|
||||
await __flushForTesting()
|
||||
expect(listMock).toHaveBeenCalledTimes(1)
|
||||
expect(pushMock).not.toHaveBeenCalled()
|
||||
expect(await pendingDirty()).toEqual(['sp'])
|
||||
|
||||
// A page that heard the workspace keeps backups pushes without asking again.
|
||||
__resetMirrorForTesting()
|
||||
pushMock.mockResolvedValue({ enabled: true, results: [{ id: 'sp' }] })
|
||||
await __flushForTesting()
|
||||
await putSession({ ...s, summary: 'changed' })
|
||||
await __flushForTesting()
|
||||
expect(listMock).toHaveBeenCalledTimes(2)
|
||||
expect(pushMock).toHaveBeenCalledTimes(2)
|
||||
expect(await pendingDirty()).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the marks when the push fails, and stops for a workspace without storage', async () => {
|
||||
const s: Session = { id: 's2', name: 'session-2', createdAt: 1, workspace_id: 'ws' }
|
||||
const never: Session = { id: 's2b', name: 'session-3', createdAt: 2, workspace_id: 'ws' }
|
||||
@@ -355,11 +380,11 @@ describe('sessionMirror flush', () => {
|
||||
|
||||
// Backups an admin turns on again elsewhere are noticed once the page's memory of
|
||||
// them being off expires, with nothing else prompting it: the removal goes then.
|
||||
listMock.mockResolvedValue({ enabled: true, sessions: [] })
|
||||
listMock.mockClear()
|
||||
pushMock.mockResolvedValueOnce({ enabled: true, results: [{ id: 's2' }] })
|
||||
await vi.waitFor(() => expect(pushMock).toHaveBeenCalledTimes(4), { timeout: 3000 })
|
||||
expect(pushMock.mock.calls[3][0].requestBody.removed).toEqual(['s2'])
|
||||
await vi.waitFor(() => expect(listMock).toHaveBeenCalledTimes(1))
|
||||
await vi.waitFor(() => expect(listMock).toHaveBeenCalled())
|
||||
await __settleForTesting()
|
||||
expect(pendingKeys()).toEqual([])
|
||||
})
|
||||
@@ -426,7 +451,7 @@ describe('sessionMirror flush', () => {
|
||||
await __flushForTesting()
|
||||
expect(pushMock).toHaveBeenCalledTimes(2)
|
||||
|
||||
listMock.mockResolvedValue({ enabled: true, sessions: [] })
|
||||
listMock.mockClear()
|
||||
pushMock.mockResolvedValueOnce({ enabled: true, results: [{ id: 'so' }] })
|
||||
usersWorkspaceStore.set({ email: EMAIL, workspaces: [] } as never)
|
||||
backupSettingsChanged('ws')
|
||||
|
||||
Reference in New Issue
Block a user