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.
213 lines
13 KiB
Markdown
213 lines
13 KiB
Markdown
---
|
|
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`:
|
|
1. **Fix Parent-Child Lifecycle Propagation**: Correctly handle `REMOVE_SELF_AND_CHILDREN` when triggered by child tasks (`WithParent`).
|
|
2. **Synchronize DB Persistence on Removal**: Ensure `PersistableTask` entries are purged from the database via `TaskService.unpersistTask()` when removed.
|
|
3. **Prevent Memory Leaks in Pending Tasks**: Unify `pendingTasks` tracking and ensure `TaskCleanup` cleans up expired pending step tasks.
|
|
4. **Fix Concurrency & Lock Cleanup Hazards**: Eliminate race conditions where lock eviction during task execution allows duplicate concurrent runs.
|
|
5. **Optimize Lookup Performance**: Replace eager full-collection queries with short-circuiting streams for partial ID lookups.
|
|
6. **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.TaskManager` interface.
|
|
- `me.bobert.netneutrality.core.tasks.TaskManagerImpl` implementation.
|
|
- `me.bobert.netneutrality.scheduled.cleanup.TaskCleanup` expiration 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 `OTask` entity or database migrations.
|
|
|
|
### Functional Requirements
|
|
- **FR-1**: When any child task implementing `WithParent` returns `Lifecycle.REMOVE_SELF_AND_CHILDREN`, `TaskManager` must remove the parent task and all associated child tasks from the registry.
|
|
- **FR-2**: When any `PersistableTask` is removed from `TaskManager` (via lifecycle completion, manual removal, or cancellation), `TaskManager` must call `taskService.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 `TaskCleanup` if abandoned.
|
|
- **FR-4**: Concurrent task invocations sharing the same `lockId` must be serialized safely without lock collisions or premature lock disposal.
|
|
- **FR-5**: `findTaskByPartialId` must short-circuit upon finding the first match without allocating full intermediate lists.
|
|
- **FR-6**: Interaction deletion via `Deletable` must 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`: `TaskService` for loading and persisting `PersistableTask` entities.
|
|
|
|
### Key Issues & Decisions
|
|
|
|
#### 1. Parent/Child Task Lifecycle Resolution
|
|
- **Issue**: `handleTaskLifecycle` only checks `if (task instanceof WithChildren)` for `REMOVE_SELF_AND_CHILDREN`. When a button task (which implements `WithParent<P>`, such as `LoadTrackFromPlaylistTask`) returns `REMOVE_SELF_AND_CHILDREN`, the check evaluates to `false`, leaving the parent and sibling buttons active in memory.
|
|
- **Decision**: In `handleTaskLifecycle`, check if the task implements `WithParent<?>`. 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 implements `WithChildren`, remove all its child tasks and itself.
|
|
|
|
#### 2. Automatic Persistence Synchronization
|
|
- **Issue**: `docs/tasks.md` specifies that persistable tasks removed after execution are deleted from the database. Currently `TaskManagerImpl.removeTask` only removes from `tasks` map and never calls `taskService.unpersistTask(persistableTask)`.
|
|
- **Decision**: In `removeTask(Task task)`, check `if (task instanceof PersistableTask persistableTask)` and invoke `taskService.unpersistTask(persistableTask)`.
|
|
|
|
#### 3. Pending Task Registry & Cleanup
|
|
- **Issue**: `pendingTasks` is stored in a `CopyOnWriteArrayList<Task>`. `getTasks()` ignores it, preventing `TaskCleanup` from pruning expired step tasks. `removeTask()` also ignores `pendingTasks`.
|
|
- **Decision**: Store `pendingTasks` in a `ConcurrentHashMap<String, Task>` (or unify with active tasks / pending lookup). Update `getTasks()` or provide a method to include pending tasks in cleanup routines. Update `removeTask(Task task)` to remove from `pendingTasks` as well as `tasks`.
|
|
|
|
#### 4. Concurrency & Lock Management
|
|
- **Issue**: `removeTask` calls `tasksLock.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 from `computeIfAbsent`, 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**: `findTaskByPartialId` calls `findTasksByPartialId`, instantiating and populating a full `ArrayList` before taking the first element.
|
|
- **Decision**: Implement `findTaskByPartialId` with `.filter(...).findFirst()` directly on the stream.
|
|
|
|
### Architecture & Component Interaction Diagram
|
|
|
|
```mermaid
|
|
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`
|
|
```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
|
|
1. **Task Registration & Lookup**:
|
|
- Insert standard `Task`, verify `getTask(id)` and `hasTask(id)` return `true`.
|
|
- Insert duplicate task ID, verify rejected with user notification.
|
|
2. **Parent / Child Lifecycle (`REMOVE_SELF_AND_CHILDREN`)**:
|
|
- Create parent task implementing `WithChildren` and child task implementing `WithParent`.
|
|
- Execute child task returning `REMOVE_SELF_AND_CHILDREN`.
|
|
- Verify parent task and all sibling child tasks are removed from the registry.
|
|
3. **Persistable Task DB Synchronization**:
|
|
- Insert `PersistableTask`, verify `TaskService.persistTask` is called.
|
|
- Remove `PersistableTask` via lifecycle completion or `removeTask()`, verify `TaskService.unpersistTask` is invoked.
|
|
4. **Step Task Flow**:
|
|
- Insert `StepTask` into pending registry.
|
|
- Reassign ID via `activateTask(task, oldId)`, verify task moves from pending to active registry under new ID.
|
|
5. **Partial ID Search**:
|
|
- Query `findTaskByPartialId` and verify it matches correct instance without scanning beyond first match.
|
|
6. **Task Expiration & Cleanup**:
|
|
- Mock expired active and pending tasks, trigger `TaskCleanup.run()`, verify expired tasks are pruned from both collections.
|
|
7. **Concurrency & Locking**:
|
|
- Simulate concurrent executions for tasks sharing a `lockId`, confirming mutual exclusion.
|
|
|
|
# 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 `handleTaskLifecycle` in `TaskManagerImpl.java` to support `REMOVE_SELF_AND_CHILDREN` for tasks implementing `WithParent`, resolving the parent task and removing both the parent and all sibling child tasks.
|
|
- Modify `removeTask(Task task)` in `TaskManagerImpl.java` to invoke `taskService.unpersistTask(persistableTask)` when `task instanceof PersistableTask`, ensuring persisted records in the database are purged when tasks finish.
|
|
- Refactor `deleteInteraction(Task task)` in `TaskManagerImpl.java` to cleanly handle `Deletable` logic for both standalone and parented tasks, adding failure logging to `InteractionHook.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 `pendingTasks` in `TaskManagerImpl.java` to use a `ConcurrentHashMap<String, Task>` keyed by task ID for O(1) lookups and consistent state management.
|
|
- Update `removeTask(Task task)`, `getTask(String id)`, and `getTasks()` in `TaskManagerImpl.java` to include pending tasks so they can be retrieved, removed, and cleaned up when expired.
|
|
- Update `TaskCleanup.java` to 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 `findTaskByPartialId` in `TaskManagerImpl.java` to use `.findFirst()` directly rather than allocating a full list via `findTasksByPartialId`.
|
|
- 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.java` using 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 and `REMOVE_SELF_AND_CHILDREN` cascading removals.
|
|
- Implement tests for `StepTask` lifecycle (pending insertion, ID reassignment via `activateTask`, and expiration).
|
|
- Implement tests for `PersistableTask` lifecycle, verifying automatic database persistence on insert and unpersisting on removal.
|
|
- Implement tests verifying thread synchronization and lock acquisition during task execution. |