Skip to main content

Event system

Everything significant runs asynchronously through RabbitMQ. A trigger (API call, scheduler, scan) calls MessageSender.send(), which routes the event to its queue; a Handle<T> implementation in disk/, worker/, search/ or transcoder/ consumes it, does its work, and may send further events. The full event overview shows how the main triggers fan out through the handlers.

The Handle<T> contract

Handle<T extends MessageData> (core module) is the central interface. handles() returns the EventType the handler owns; the default listener() rejects any message whose eventType field does not match before dispatching to handle(). Handlers are plain Spring beans with a @RabbitListener on their queue.

No Hibernate session on listener threads. RabbitMQ listener threads have no open session, so lazy association navigation throws LazyInitializationException. Handlers must load what they need with explicit repository queries (fetch joins or dedicated finder methods), never by walking entity graphs.

Two enums, do not confuse them

  • EventType (database/.../enums/EventType.java) is the logical message type — the source of truth for what kinds of events exist (31 values). Handle.handles() returns one.
  • MessageQueue (core/.../MessageQueue.java) holds the queue base names. MessageSender maps an event to its queue.

Queue names follow app.ister.server.<event>[.<scope>], where the scope is a directory name, node name, or absent for global queues. Scoping is what routes work to the node that owns the files: each node declares and listens only on the queues for its own directories.

Retries and dead-lettering

Failed listeners retry with exponential backoff (spring.rabbitmq.listener.simple.retry.* in core.properties: 3 attempts, 2s initial interval, multiplier 2). After the final failure a RepublishMessageRecoverer moves the message to the app.ister.server.dead-letter queue with the exception preserved in the message headers (RabbitReliabilityConfig). Recent failures also feed the RecentFailuresBuffer for the status subscriptions (chapter 5). The Helm chart's e2e fails on any dead-lettered event, which is why every external call must sit behind a configurable base URL.

Queue scoping

ScopeEvents
Node .{nodeName}ANALYZE_LIBRARY_REQUEST
Directory .{dirName}NEW_DIRECTORIES_SCAN_REQUEST, FILE_SCAN_REQUESTED, MEDIA_FILE_FOUND, AUDIO_FILE_FOUND, EPUB_FILE_FOUND, COMIC_FILE_FOUND, SUBTITLE_FILE_FOUND, IMAGE_FOUND, NFO_FILE_FOUND, UPDATE_IMAGES_REQUESTED, ANALYZE_DATA (disk), PRE_TRANSCODE_RECENTLY_WATCHED, TRANSCODE_REQUESTED, TRANSCODE_PASS_REQUESTED
GlobalSHOW_FOUND, EPISODE_FOUND, MOVIE_FOUND, PERSON_FOUND, ALBUM_FOUND, BOOK_FOUND, COMIC_SERIES_FOUND, CHAPTER_FOUND (no consumer), PODCAST_FOUND (no consumer), PODCAST_EPISODE_FOUND (no consumer), PODCAST_REFRESH_REQUESTED, CONTINUE_WATCHING_REBUILD_REQUESTED, ANALYZE_DATA (worker), SEARCH_INDEX_REQUESTED, SEARCH_REINDEX_REQUESTED
Cache directory .{nodeName}-cache-directoryPODCAST_EPISODE_DOWNLOAD_REQUESTED (the download lands on that node's disk)

PRE_TRANSCODE_RECENTLY_WATCHED is scoped by disk name rather than directory name (see TranscoderQueueNamingConfig).

Handler reference

HandlerModuleReceivesSends
HandleNewDirectoriesScanRequesteddiskNEW_DIRECTORIES_SCAN_REQUESTFILE_SCAN_REQUESTED
FileScanRequestedHandlediskFILE_SCAN_REQUESTEDMEDIA_FILE_FOUND / AUDIO_FILE_FOUND / EPUB_FILE_FOUND / COMIC_FILE_FOUND / IMAGE_FOUND / NFO_FILE_FOUND / SUBTITLE_FILE_FOUND
HandleMediaFileFounddiskMEDIA_FILE_FOUNDIMAGE_FOUND
HandleAudioFileFounddiskAUDIO_FILE_FOUNDIMAGE_FOUND (track- or chapter-bound, by library type)
HandleEpubFileFounddiskEPUB_FILE_FOUNDIMAGE_FOUND
HandleComicFileFounddiskCOMIC_FILE_FOUNDIMAGE_FOUND (extracted cover)
HandleSubtitleFileFounddiskSUBTITLE_FILE_FOUND
HandleImageFounddiskIMAGE_FOUND
HandleNfoFileFounddiskNFO_FILE_FOUND
HandleUpdateImagesRequesteddiskUPDATE_IMAGES_REQUESTEDUPDATE_IMAGES_REQUESTED (next chunk)
HandleAnalyzeDataDiskdiskANALYZE_DATAMEDIA_FILE_FOUND / AUDIO_FILE_FOUND / NFO_FILE_FOUND / SUBTITLE_FILE_FOUND
HandlePreTranscodeRecentlyWatcheddiskPRE_TRANSCODE_RECENTLY_WATCHEDTRANSCODE_REQUESTED, MEDIA_FILE_FOUND (for files without analyzed streams)
HandlePersonFounddiskPERSON_FOUNDNFO_FILE_FOUND
HandleAlbumFounddiskALBUM_FOUNDNFO_FILE_FOUND
HandlePodcastEpisodeDownloadRequesteddiskPODCAST_EPISODE_DOWNLOAD_REQUESTEDAUDIO_FILE_FOUND (on the cache-dir queue → ffprobe + HLS pre-generation)
AnalyzeLibraryRequestedHandleworkerANALYZE_LIBRARY_REQUESTUPDATE_IMAGES_REQUESTED, SHOW_FOUND, EPISODE_FOUND, MOVIE_FOUND, PERSON_FOUND, ALBUM_FOUND, AUDIO_FILE_FOUND
AnalyzeDataHandleworkerANALYZE_DATAcascade per entity type
HandleShowFoundworkerSHOW_FOUNDIMAGE_FOUND (+ cast credits written directly to the database)
HandleEpisodeFoundworkerEPISODE_FOUNDIMAGE_FOUND (+ cast/guest-star credits directly to the database)
MovieFoundHandleworkerMOVIE_FOUNDIMAGE_FOUND (+ cast credits directly to the database)
HandlePersonFoundworkerPERSON_FOUND
HandleAlbumFoundworkerALBUM_FOUNDIMAGE_FOUND
HandleBookFoundworkerBOOK_FOUNDIMAGE_FOUND (Open Library cover, only when none exists yet)
HandleComicSeriesFoundworkerCOMIC_SERIES_FOUNDIMAGE_FOUND (Wikipedia thumbnail, only when no local artwork)
HandlePodcastRefreshRequestedworkerPODCAST_REFRESH_REQUESTEDIMAGE_FOUND (feed cover), PODCAST_EPISODE_FOUND, PODCAST_EPISODE_DOWNLOAD_REQUESTED (newest N)
HandleContinueWatchingRebuildRequestedworkerCONTINUE_WATCHING_REBUILD_REQUESTED
HandleTranscodeRequestedtranscoderTRANSCODE_REQUESTEDTRANSCODE_PASS_REQUESTED
HandleTranscodePassRequestedtranscoderTRANSCODE_PASS_REQUESTED
HandleSearchIndexRequestedsearchSEARCH_INDEX_REQUESTED— (upsert/delete in Typesense)
HandleSearchReindexRequestedsearchSEARCH_REINDEX_REQUESTED— (full rebuild + alias swap)

SEARCH_INDEX_REQUESTED is emitted from many places: ServerEventService.createXFoundEvent (on creation), MetadataSave (TMDB), the MusicBrainz and NFO handlers, audio-tag saves (including action=DELETE on track dedup), and metadata deletes — see chapter 6.