commit 734014cedecabe12fecb96a37fc38cbf0fd072c1 from: mtmn date: Thu Jun 25 12:29:05 2026 UTC fix: kill sync loop fibers on shutdown commit - 0cab7a6b2a82d6a3d7694db9a63775bc9712b689 commit + 734014cedecabe12fecb96a37fc38cbf0fd072c1 blob - a046c5b38ceac71fde10ad03e5e7067730439258 blob + 9f8d7db2bb5428180b5d89637e21b7ae4c035982 --- src/Main.purs +++ src/Main.purs @@ -55,6 +55,7 @@ type UserContext = , displayName :: String , enrichMetadataFiber :: Maybe (Fiber Unit) , backupFiber :: Maybe (Fiber Unit) + , syncFibers :: Array (Fiber Unit) } normalizePath :: String -> String @@ -397,22 +398,31 @@ startUser { slug, name, config } = do Just lfmUser, Just apiKey -> Just <$> forkAff (lfSync conn apiKey lfmUser slug writeLock) _, _ -> pure Nothing + -- Join the initial sync fibers so their completion is logged before loops start. void $ forkAff do for_ lbFiber joinFiber for_ lfFiber joinFiber - for_ config.listenbrainzUser \username -> - void $ forkAff $ lbSyncLoop conn username slug writeLock - case config.lastfmUser, config.lastfmApiKey of - Just lfmUser, Just apiKey -> void $ forkAff $ lfSyncLoop conn apiKey lfmUser slug writeLock - _, _ -> pure unit + -- Spawn the recurring sync loops eagerly; each starts with a 60s delay so + -- they won't race with the initial sync above. Track them so cleanupUser + -- can kill them on shutdown. + loopFibers <- traverse forkAff + $ + [ case config.listenbrainzUser of + Just username -> Just $ lbSyncLoop conn username slug writeLock + Nothing -> Nothing + , case config.lastfmUser, config.lastfmApiKey of + Just lfmUser, Just apiKey -> Just $ lfSyncLoop conn apiKey lfmUser slug writeLock + _, _ -> Nothing + ] # Data.Array.mapMaybe identity + enrichMetadataFiber <- forkAff $ enrichMetadata conn config slug backupFiber <- if config.backupEnabled then Just <$> forkAff (backupDb conn config.databaseFile (s3ConfigFromUser config) (Int.toNumber config.backupIntervalHours * 3600000.0) slug) else pure Nothing let displayName = fromMaybe (if slug == "" then "root" else slug) name - pure { conn, writeLock, config, slug, displayName, enrichMetadataFiber: Just enrichMetadataFiber, backupFiber } + pure { conn, writeLock, config, slug, displayName, enrichMetadataFiber: Just enrichMetadataFiber, backupFiber, syncFibers: loopFibers } cleanupUser :: UserContext -> Aff Unit cleanupUser ctx = do @@ -422,6 +432,8 @@ cleanupUser ctx = do void $ try $ killFiber (Exception.error "Server shutting down") fiber for_ ctx.backupFiber \fiber -> void $ try $ killFiber (Exception.error "Server shutting down") fiber + for_ ctx.syncFibers \fiber -> + void $ try $ killFiber (Exception.error "Server shutting down") fiber Log.info $ "Closed user: " <> label foreign import dotenvConfig :: Effect Unit blob - 784967cb6fcddfccd29f459da314d8089529f650 blob + 8e8fad7726de0b1cc40222bf74c0dba363919439 --- test/Main.purs +++ test/Main.purs @@ -2,12 +2,12 @@ module Test.Main where import Prelude -import Unsafe.Coerce (unsafeCoerce) import Data.Argonaut (decodeJson, encodeJson, parseJson) import Data.Array (length) import Data.Either (Either(..), isRight) import Data.Maybe (Maybe(..)) import Effect (Effect) +import Effect.Aff.AVar as Avar import Test.Spec (describe, it) import Test.Spec.Assertions (shouldEqual, fail) import Data.String.Regex (regex, parseFlags) @@ -256,8 +256,11 @@ main = runSpecAndExitProcess [consoleReporter] do , backupEnabled: false , backupIntervalHours: 0 } - ctx1 = { conn: conn1, writeLock: unsafeCoerce unit, config: dummyConfig, slug: "user1", displayName: "User 1", enrichMetadataFiber: Nothing, backupFiber: Nothing } - ctx2 = { conn: conn2, writeLock: unsafeCoerce unit, config: dummyConfig, slug: "user2", displayName: "User 2", enrichMetadataFiber: Nothing, backupFiber: Nothing } + lock1 <- Avar.new unit + lock2 <- Avar.new unit + let + ctx1 = { conn: conn1, writeLock: lock1, config: dummyConfig, slug: "user1", displayName: "User 1", enrichMetadataFiber: Nothing, backupFiber: Nothing, syncFibers: [] } + ctx2 = { conn: conn2, writeLock: lock2, config: dummyConfig, slug: "user2", displayName: "User 2", enrichMetadataFiber: Nothing, backupFiber: Nothing, syncFibers: [] } contexts = [ ctx1, ctx2 ] res1 <- findUserByToken contexts token1