Reworked `Settings` class to replace the static singleton pattern with constructor injection of `SettingsProvider`, improving testability and modularity. Updated all affected classes, tests, and configurations to align with the new architecture.
8.1 KiB
sessionId
| sessionId |
|---|
| session-260906-073726-sbxb |
Requirements
Overview & Goals
Improve the code quality, correctness, and maintainability of QueueManager, TrackScheduler, TrackInfo, and VoteSkipService — the core music queue and playback scheduling subsystem.
Scope
In Scope:
- Fix thread-safety issues (check-then-act race conditions in
QueueManager) - Simplify confusing queue insertion logic in
QueueManager.queue() - Separate concerns: extract command-result formatting from queue data operations
- Fix
TrackInfousing wrong logger (QueueManager.classinstead ofTrackInfo.class) - Clean up
TrackScheduler.onTrackStuck()complexity - Add unit tests for
QueueManager,TrackScheduler,TrackInfo, andVoteSkipService - Expose skip-vote state through
TrackInfomore cleanly (encapsulate theSet<String>skips)
Out of Scope:
- Changes to
MusicManager,MusicServiceImpl, or audio source/loading classes - New user-facing features or commands
- Changes to
MusicManagerCallbackinterface
Technical Design
Current Implementation
QueueManager (src/main/java/me/bobert/netneutrality/music/QueueManager.java, 286 lines):
- Manages a
LinkedBlockingDeque<TrackInfo>with manualsynchronizedblocks - Multiple methods check state outside the synchronized block, then re-check inside (e.g.,
clearQueue()L81 vs L86,skip()L197 vs L203,shuffle()L229 vs L233,removeFromQueue()L254 vs L259) — the outer checks are racy and misleading queue()method (L135-158) has confusing branching:addFirst+addcan result in the same track being added twice whenplayer.getPlayingTrack() == null && !queue.isEmpty()clearQueue()callscallback.onQueueEmpty()before checking if queue is empty (L79)removeTracksFromUser()iterates the queue outside synchronized (L97), then again inside (L102)skip(int)silently resetsnbTrackto 1 if it exceeds queue size (L209) — surprising behavior- Mixes queue data operations with
CommandExecutionResultformatting
TrackScheduler (src/main/java/me/bobert/netneutrality/music/TrackScheduler.java, 175 lines):
onTrackStuck()(L129-173) has deeply nested logic with inline async recovery — hard to follow and test- Circular dependency: creates
QueueManagerin constructor, which takesthisback contextfield is accessed from multiple threads but onlyattachContext/detachContextare synchronized —getContext(),getDj()are not
TrackInfo (src/main/java/me/bobert/netneutrality/music/TrackInfo.java, 83 lines):
- Logger incorrectly uses
QueueManager.class(L26) instead ofTrackInfo.class getSkips()returns mutableSet<String>— breaks encapsulation- Duplicate copyright block (L1-5 and L7-11)
VoteSkipService (src/main/java/me/bobert/netneutrality/music/VoteSkipService.java, 57 lines):
- Relatively clean, but directly calls
queueManager.skip()mixing vote logic with queue mutation info.addSkip()usescontext.getGuildUser()butinfo.getSkips().size()check uses raw set — no race protection
Proposed Changes
QueueManager
- Remove all outer unsynchronized checks — move all logic inside a single
synchronizedblock per method to eliminate check-then-act races - Rewrite
queue()method — simplify to: add to queue, if nothing playing start the head track. Remove the confusingaddFirst/adddual-path - Fix
clearQueue()— movecallback.onQueueEmpty()call to after the queue is confirmed non-empty and cleared - Fix
skip(int)— whennbTrack > queue.size(), skip all remaining tracks instead of silently resetting to 1 - Fix
removeTracksFromUser()— do all filtering inside synchronized block - Return queue operation results as data (e.g., number removed, track name) and let callers build
CommandExecutionResult— or at minimum, document that this class returns command results
TrackScheduler
- Extract stuck-track recovery into a dedicated
StuckTrackRecoveryHandlerclass to reduceonTrackStuck()complexity - Make
contextaccess thread-safe — synchronizegetContext()andgetDj(), or usevolatile - Add
lastStuckTrackreset on track start or successful recovery to prevent stale state
TrackInfo
- Fix logger — change to
TrackInfo.class - Remove duplicate copyright block
- Encapsulate skips — return
Collections.unmodifiableSet(skips)fromgetSkips(), addgetSkipCount()method
VoteSkipService
- Synchronize vote-skip check — ensure
addSkip+ threshold check is atomic (or accept eventual consistency and document it)
File Structure
All changes in src/main/java/me/bobert/netneutrality/music/:
- Modified:
QueueManager.java,TrackScheduler.java,TrackInfo.java,VoteSkipService.java - New:
StuckTrackRecoveryHandler.java - New tests:
src/test/java/me/bobert/netneutrality/command/music/QueueManagerTest.java,TrackSchedulerTest.java,TrackInfoTest.java,VoteSkipServiceTest.java
Testing
Validation Approach
Add unit tests for all four classes using JUnit 5 and Mockito (already in project dependencies via the existing test classes).
Key Scenarios
- QueueManager: queue/dequeue ordering, skip single/multiple, shuffle preserves current track, removeFromQueue bounds, clearQueue idempotency, removeTracksFromUser correctness
- TrackInfo: skip vote tracking, duplicate skip prevention, unmodifiable skips set, equals/hashCode
- VoteSkipService: threshold calculation, skip when below min members, vote counting, duplicate vote handling
- TrackScheduler: onTrackEnd triggers correct queue action per end reason, onTrackStuck recovery flow
Edge Cases
- Queue with single track: skip, shuffle, remove
- Skip count exceeding queue size
- Empty queue operations
- Concurrent vote-skip from same user
Delivery Steps
✓ Step 1: Fix TrackInfo issues
TrackInfo has correct logger, clean copyright, and encapsulated skip state.
- Fix logger from
QueueManager.classtoTrackInfo.class(line 26) - Remove duplicate copyright block (lines 7-11)
- Change
getSkips()to returnCollections.unmodifiableSet(skips) - Add
getSkipCount()convenience method - Add
TrackInfoTest.javacovering skip tracking, duplicate prevention, equals/hashCode, and unmodifiable set enforcement
✓ Step 2: Fix QueueManager thread-safety and queue logic
QueueManager has correct synchronization and simplified queue insertion.
- Remove all outer unsynchronized pre-checks in
clearQueue(),skip(),shuffle(),removeFromQueue(),removeTracksFromUser()— consolidate into single synchronized blocks - Rewrite
queue(AudioTrack, Member)to eliminate the confusingaddFirst/adddual-path: simply add to tail, then start playback if nothing is playing - Fix
clearQueue()to callcallback.onQueueEmpty()only after confirming queue was non-empty - Fix
skip(int)to skip all remaining whennbTrack > queue.size()instead of silently resetting to 1 - Fix
removeTracksFromUser()to do all filtering inside synchronized block - Add
QueueManagerTest.javacovering queue ordering, skip, shuffle, remove, clear, and edge cases
✓ Step 3: Extract stuck-track recovery and fix TrackScheduler thread-safety
TrackScheduler is simplified with extracted recovery logic and thread-safe context access.
- Create
StuckTrackRecoveryHandler.javaencapsulating the retry-or-skip logic currently inonTrackStuck()(lines 129-173) - Simplify
onTrackStuck()to delegate to the new handler - Reset
lastStuckTrackinonTrackStart()to prevent stale state - Synchronize
getContext()andgetDj()methods (or makecontextvolatile) - Add
TrackSchedulerTest.javacovering onTrackEnd behavior per end reason and stuck-track recovery delegation
✓ Step 4: Improve VoteSkipService robustness and add tests
VoteSkipService has documented concurrency behavior and full test coverage.
- Add documentation comment about the vote-then-check atomicity assumption
- Add
VoteSkipServiceTest.javacovering: threshold math, auto-skip below min members, vote accumulation, duplicate vote from same user, skip trigger when threshold exceeded