From ec888d3a2600e6f9823193be0d25f46a3e13d5e7 Mon Sep 17 00:00:00 2001 From: onysd Date: Thu, 3 Sep 2026 08:42:01 +0300 Subject: [PATCH] fix for files purge --- cmd/telesrv/main.go | 19 +++++++++++++------ internal/app/files/retention_purge.go | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/cmd/telesrv/main.go b/cmd/telesrv/main.go index 301b6d3d..84c6dc95 100644 --- a/cmd/telesrv/main.go +++ b/cmd/telesrv/main.go @@ -1128,7 +1128,10 @@ func run(logger *zap.Logger) error { // Active eviction is independent of TELESRV_STORAGE_RETENTION_MODE (can // run even when that's "off") and reuses the same media sweep ticker. retentionWorker = retentionWorker.WithStorageEviction(filesService, cfg.StorageEvictionEnable) - go retentionWorker.Run(ctx) + // retentionWorker.Run itself isn't started here -- see the + // filesService.SetRetentionPurgeNotifier call below, which must happen + // first so the worker's very first (synchronous) sweep tick can't purge + // blobs before there's anywhere to send the purge notice. go filesapp.NewUploadPartGCWorker(filesService, logger.Named("files").Named("upload_gc"), cfg.UploadPartTTL, cfg.UploadPartGCInterval, @@ -1425,12 +1428,16 @@ func run(logger *zap.Logger) error { messageapp.WithBusinessAutomation(passwordStore, businessAutomationOptions...), ) // Wires the storage retention sweep's purge-notice capability now that - // both edit-capable app services exist -- filesService (and the - // background retentionWorker goroutine reading it) was constructed - // earlier, before either was available. A sweep tick that races ahead of - // this call simply finds no notifier yet and skips the notice for that - // tick (best-effort, see files.SetRetentionPurgeNotifier). + // both edit-capable app services exist -- filesService was constructed + // earlier, before either was available. Must happen before + // retentionWorker.Run starts below: that call's first sweep tick runs + // synchronously (maintenance.RetentionWorker.Run -> runOnce), and once a + // document/photo's blob bytes are purged it never again matches the + // hard-retention candidate query (see ListDocumentIDsForHardRetentionOlderThan's + // doc comment) -- so a tick that raced ahead of this call wouldn't just + // delay the notice, it would permanently lose it (files.SetRetentionPurgeNotifier). filesService.SetRetentionPurgeNotifier(messagesService, channelsService) + go retentionWorker.Run(ctx) moderationService := moderationapp.NewService( moderationReportStore, moderationapp.WithMessageReaders(messagesService, channelsService), diff --git a/internal/app/files/retention_purge.go b/internal/app/files/retention_purge.go index a18cba08..800d6f48 100644 --- a/internal/app/files/retention_purge.go +++ b/internal/app/files/retention_purge.go @@ -36,13 +36,18 @@ type RetentionPurgeChannelEditor interface { // capability the storage retention sweep needs to turn a hard-retention/ // eviction blob purge into a visible messageActionCustomAction notice on // every message still embedding the purged document/photo. Both app-layer -// services are constructed after this Service in cmd/telesrv/main.go (and -// the background retention sweep goroutine is started before either exists), -// so this is a post-construction setter rather than a NewService option: a -// sweep tick that races ahead of this call simply finds both fields nil and -// skips the notice for that tick, same as any other per-reference failure -// below (best-effort -- the underlying blob purge has already committed and -// must never be undone or retried because of a notice failure). +// services are constructed after this Service in cmd/telesrv/main.go, so +// this is a post-construction setter rather than a NewService option -- +// cmd/telesrv/main.go deliberately calls this BEFORE starting the +// retentionWorker.Run goroutine, since a sweep tick that raced ahead of this +// call would find both fields nil and permanently lose the notice for +// whatever it purged that tick (a purged document/photo never again matches +// the hard-retention candidate query, so there's no later tick to retry the +// notice on). If both fields are still nil here for some other reason (e.g. +// a future caller reordering), the notice is best-effort and silently +// skipped, same as any other per-reference failure below -- the underlying +// blob purge has already committed and must never be undone or retried +// because of a notice failure. func (s *Service) SetRetentionPurgeNotifier(messages RetentionPurgeMessageEditor, channels RetentionPurgeChannelEditor) { s.retentionNotifyMu.Lock() defer s.retentionNotifyMu.Unlock()