Redesigned task lifecycle management to properly cascade parent-child removals and synchronize database persistence on task deletion. Refactored task management to address memory leaks, concurrency collisions, and pending-task cleanup issues. Enhanced lookup performance, logging accuracy, and test coverage with a comprehensive unit test suite validating all task scenarios.
13 KiB
sessionId
| sessionId |
|---|
| session-260919-132150-1vmv |
Requirements
Overview & Goals
The TaskManager subsystem in Net Neutrality manages the in-memory registry, execution dispatch, locking, persistence, and lifecycle transitions for Discord interaction tasks (buttons, modals, reactions, step-by-step flows, and scheduled background tasks).
This plan addresses several bugs, synchronization oversights, memory leak potentials, and architectural discrepancies between docs/tasks.md and TaskManagerImpl.java:
- Fix Parent-Child Lifecycle Propagation: Correctly handle
REMOVE_SELF_AND_CHILDRENwhen triggered by child tasks (WithParent). - Synchronize DB Persistence on Removal: Ensure
PersistableTaskentries are purged from the database viaTaskService.unpersistTask()when removed. - Prevent Memory Leaks in Pending Tasks: Unify
pendingTaskstracking and ensureTaskCleanupcleans up expired pending step tasks. - Fix Concurrency & Lock Cleanup Hazards: Eliminate race conditions where lock eviction during task execution allows duplicate concurrent runs.
- Optimize Lookup Performance: Replace eager full-collection queries with short-circuiting streams for partial ID lookups.
- Improve Test Coverage: Introduce thorough unit tests covering all task lifecycle paths, locking behavior, and persistence hooks.
Scope
- In Scope:
me.bobert.netneutrality.core.tasks.TaskManagerinterface.me.bobert.netneutrality.core.tasks.TaskManagerImplimplementation.me.bobert.netneutrality.scheduled.cleanup.TaskCleanupexpiration handling.- Interaction deletion error-handling callbacks on
InteractionHook. - Comprehensive unit test suite for
TaskManagerImpl.
- Out of Scope:
- Refactoring concrete task business logic (e.g., individual music or reaction commands).
- Schema changes to the
OTaskentity or database migrations.
Functional Requirements
- FR-1: When any child task implementing
WithParentreturnsLifecycle.REMOVE_SELF_AND_CHILDREN,TaskManagermust remove the parent task and all associated child tasks from the registry. - FR-2: When any
PersistableTaskis removed fromTaskManager(via lifecycle completion, manual removal, or cancellation),TaskManagermust calltaskService.unpersistTask()so the task is deleted from persistent storage. - FR-3: Pending step tasks must be tracked reliably, exposed for expiration checks, and cleaned up by
TaskCleanupif abandoned. - FR-4: Concurrent task invocations sharing the same
lockIdmust be serialized safely without lock collisions or premature lock disposal. - FR-5:
findTaskByPartialIdmust short-circuit upon finding the first match without allocating full intermediate lists. - FR-6: Interaction deletion via
Deletablemust gracefully handle expired hooks and Discord API errors asynchronously.
Technical Design
Current Implementation
TaskManagerImpl (me.bobert.netneutrality.core.tasks.TaskManagerImpl) implements TaskManager and Lifecycle. It maintains:
tasks:ConcurrentHashMap<String, Task>for active tasks.pendingTasks:CopyOnWriteArrayList<Task>for unassigned step tasks.deletableTasksContext:ConcurrentHashMap<String, CommandContext>for tracking interaction hooks to delete.tasksLock:ConcurrentHashMap<String, ReentrantLock>for per-task/per-parent synchronization.taskService:TaskServicefor loading and persistingPersistableTaskentities.
Key Issues & Decisions
1. Parent/Child Task Lifecycle Resolution
- Issue:
handleTaskLifecycleonly checksif (task instanceof WithChildren)forREMOVE_SELF_AND_CHILDREN. When a button task (which implementsWithParent<P>, such asLoadTrackFromPlaylistTask) returnsREMOVE_SELF_AND_CHILDREN, the check evaluates tofalse, leaving the parent and sibling buttons active in memory. - Decision: In
handleTaskLifecycle, check if the task implementsWithParent<?>. If so, obtain the parent task (taskWithParent.getParentTask()) and remove the parent along with all of its child tasks (parent.getChildTasks()). If the task directly implementsWithChildren, remove all its child tasks and itself.
2. Automatic Persistence Synchronization
- Issue:
docs/tasks.mdspecifies that persistable tasks removed after execution are deleted from the database. CurrentlyTaskManagerImpl.removeTaskonly removes fromtasksmap and never callstaskService.unpersistTask(persistableTask). - Decision: In
removeTask(Task task), checkif (task instanceof PersistableTask persistableTask)and invoketaskService.unpersistTask(persistableTask).
3. Pending Task Registry & Cleanup
- Issue:
pendingTasksis stored in aCopyOnWriteArrayList<Task>.getTasks()ignores it, preventingTaskCleanupfrom pruning expired step tasks.removeTask()also ignorespendingTasks. - Decision: Store
pendingTasksin aConcurrentHashMap<String, Task>(or unify with active tasks / pending lookup). UpdategetTasks()or provide a method to include pending tasks in cleanup routines. UpdateremoveTask(Task task)to remove frompendingTasksas well astasks.
4. Concurrency & Lock Management
- Issue:
removeTaskcallstasksLock.remove(task.getLockId())immediately. If another thread is waiting or about to acquire the lock for a sibling task, it can get a new lock instance fromcomputeIfAbsent, breaking mutual exclusion. - Decision: Maintain lock instances safely; do not remove locks eagerly while in the middle of executing handlers. Persistable tasks with state should also participate in locking if needed, or maintain clear locking semantics.
5. Lookup API Optimization
- Issue:
findTaskByPartialIdcallsfindTasksByPartialId, instantiating and populating a fullArrayListbefore taking the first element. - Decision: Implement
findTaskByPartialIdwith.filter(...).findFirst()directly on the stream.
Architecture & Component Interaction Diagram
graph TD
subgraph Discord Events
BE[ButtonInteractionEvent] --> EH[EventsHandler]
ME[ModalInteractionEvent] --> EH
RE[MessageReactionAddEvent] --> EH
MSG[MessageReceivedEvent] --> EH
end
subgraph TaskManager Subsystem
EH -->|TaskContext| TM[TaskManagerImpl]
TM -->|Acquire Lock| TL[tasksLock Map]
TM -->|Lookup Task| TR[tasks & pendingTasks Maps]
TM -->|Execute| T[Task.execute]
T -->|Return Lifecycle| TM
TM -->|Cascade Cleanup| LC[handleTaskLifecycle]
LC -->|WithParent / WithChildren| RM[removeTask]
RM -->|Unpersist| TS[TaskService]
TS -->|Delete| DB[(Database / OTask)]
RM -->|Deletable| DH[InteractionHook.deleteOriginal]
end
subgraph Background Maintenance
TC[TaskCleanup] -->|Periodic Poll| TM
TM -->|Prune Expired| RM
end
Proposed Code Changes & Contracts
TaskManagerImpl.java
private void handleTaskLifecycle(Task task, Task.Lifecycle status) {
if (status == null) {
throw new IllegalArgumentException("Task lifecycle status cannot be null");
}
switch (status) {
case REMOVE_SELF -> removeTask(task);
case REMOVE_SELF_AND_CHILDREN -> {
if (task instanceof WithParent<?> taskWithParent) {
Task parent = taskWithParent.getParentTask();
if (parent instanceof WithChildren parentWithChildren) {
for (Task child : parentWithChildren.getChildTasks()) {
removeTask(child);
}
}
removeTask(parent);
} else if (task instanceof WithChildren taskWithChildren) {
for (Task child : taskWithChildren.getChildTasks()) {
removeTask(child);
}
removeTask(task);
} else {
removeTask(task);
}
}
case KEEP, PARENT, BUILTIN -> {
// Maintained in active registry
}
}
}
@Override
public void removeTask(Task task) {
if (task == null) return;
String id = task.getId();
boolean removed = (tasks.remove(id) != null);
removed |= (pendingTasks.remove(task) != null);
if (removed) {
logger.debug("Removing task: {}", id);
if (task instanceof PersistableTask persistableTask) {
taskService.unpersistTask(persistableTask);
}
}
}
File Structure Changes
- Modified:
src/main/java/me/bobert/netneutrality/core/tasks/TaskManagerImpl.java - Modified:
src/main/java/me/bobert/netneutrality/scheduled/cleanup/TaskCleanup.java - Added:
src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.java
Testing
Validation Approach
Automated unit tests will be created in src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.java utilizing JUnit 5, AssertJ, and Mockito to validate all functional requirements without requiring live Discord connections or database instances.
Key Scenarios
- Task Registration & Lookup:
- Insert standard
Task, verifygetTask(id)andhasTask(id)returntrue. - Insert duplicate task ID, verify rejected with user notification.
- Insert standard
- Parent / Child Lifecycle (
REMOVE_SELF_AND_CHILDREN):- Create parent task implementing
WithChildrenand child task implementingWithParent. - Execute child task returning
REMOVE_SELF_AND_CHILDREN. - Verify parent task and all sibling child tasks are removed from the registry.
- Create parent task implementing
- Persistable Task DB Synchronization:
- Insert
PersistableTask, verifyTaskService.persistTaskis called. - Remove
PersistableTaskvia lifecycle completion orremoveTask(), verifyTaskService.unpersistTaskis invoked.
- Insert
- Step Task Flow:
- Insert
StepTaskinto pending registry. - Reassign ID via
activateTask(task, oldId), verify task moves from pending to active registry under new ID.
- Insert
- Partial ID Search:
- Query
findTaskByPartialIdand verify it matches correct instance without scanning beyond first match.
- Query
- Task Expiration & Cleanup:
- Mock expired active and pending tasks, trigger
TaskCleanup.run(), verify expired tasks are pruned from both collections.
- Mock expired active and pending tasks, trigger
- Concurrency & Locking:
- Simulate concurrent executions for tasks sharing a
lockId, confirming mutual exclusion.
- Simulate concurrent executions for tasks sharing a
Delivery Steps
✓ Step 1: Fix Task Lifecycle Hierarchy and DB Persistence Synchronization
TaskManagerImpl correctly propagates parent-child lifecycle actions and synchronizes task deletions with TaskService.
- Update
handleTaskLifecycleinTaskManagerImpl.javato supportREMOVE_SELF_AND_CHILDRENfor tasks implementingWithParent, resolving the parent task and removing both the parent and all sibling child tasks. - Modify
removeTask(Task task)inTaskManagerImpl.javato invoketaskService.unpersistTask(persistableTask)whentask instanceof PersistableTask, ensuring persisted records in the database are purged when tasks finish. - Refactor
deleteInteraction(Task task)inTaskManagerImpl.javato cleanly handleDeletablelogic for both standalone and parented tasks, adding failure logging toInteractionHook.deleteOriginal().queue().
✓ Step 2: Refactor Pending Task Management and Concurrency Locking
Pending tasks and active task locks are managed safely without race conditions or memory leaks.
- Refactor
pendingTasksinTaskManagerImpl.javato use aConcurrentHashMap<String, Task>keyed by task ID for O(1) lookups and consistent state management. - Update
removeTask(Task task),getTask(String id), andgetTasks()inTaskManagerImpl.javato include pending tasks so they can be retrieved, removed, and cleaned up when expired. - Update
TaskCleanup.javato ensure expired pending tasks are identified and cleaned up. - Improve lock management in
TaskManagerImpl.handle(TaskContext context)so that lock eviction does not interfere with concurrently waiting threads acquiring new lock instances.
✓ Step 3: Optimize Task Query Performance and Diagnostic Logging
Task lookup methods use short-circuiting stream pipelines, and diagnostic logging is corrected.
- Optimize
findTaskByPartialIdinTaskManagerImpl.javato use.findFirst()directly rather than allocating a full list viafindTasksByPartialId. - Fix typos in log messages and warnings across
TaskManagerImpl.java(e.g."Aquiring lock","childs"). - Ensure consistent ephemeral fallback messages and handling when unacknowledged interactions or missing tasks are encountered.
✓ Step 4: Add Comprehensive Unit Tests for Task Management
A complete unit test suite validates TaskManagerImpl functionality across all task types, lifecycles, and concurrency scenarios.
- Create
src/test/java/me/bobert/netneutrality/core/tasks/TaskManagerImplTest.javausing JUnit 5 and Mockito. - Implement tests for standard task registration (
insert), lookup (getTask,findTaskByPartialId), and removal (removeTask). - Implement tests for parent-child tasks (
WithChildren,WithParent), verifying lock inheritance andREMOVE_SELF_AND_CHILDRENcascading removals. - Implement tests for
StepTasklifecycle (pending insertion, ID reassignment viaactivateTask, and expiration). - Implement tests for
PersistableTasklifecycle, verifying automatic database persistence on insert and unpersisting on removal. - Implement tests verifying thread synchronization and lock acquisition during task execution.