Skip to content

Commit

Permalink
Added rescheduleMissingTasks
Browse files Browse the repository at this point in the history
Reschedules tasks that are present in the database but missing in the downloader. The assumption is that they were lost when the app was killed
  • Loading branch information
781flyingdutchman committed Jan 14, 2025
1 parent 6a6f1ea commit 979785b
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 2 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,9 @@ To ensure your callbacks or listener capture events that may have happened when
In summary, to track your tasks persistently, follow these steps in order, immediately after app startup:
1. If using a non-default `PersistentStorage` backend, initialize with `FileDownloader(persistentStorage: MyPersistentStorage())` and wait for the initialization to complete by calling `await FileDownloader().ready` (see [using the database](#using-the-database-to-track-tasks) for details on `PersistentStorage`).
2. Register an event listener or callback(s) to process status and progress updates
3. call `await FileDownloader().trackTasks()` if you want to track the tasks in a persistent database
4. call `await FileDownloader().resumeFromBackground()` to ensure events that happened while your app was in the background are processed
3. Call `await FileDownloader().trackTasks()` if you want to track the tasks in a persistent database
4. Call `await FileDownloader().resumeFromBackground()` to ensure events that happened while your app was in the background are processed
5. If you are tracking tasks in the database, after ~5 seconds, call `await FileDownloader().rescheduleMissingTasks()` to reschedule tasks that are in the database as `enqueued` or `running` yet are not enqueued or running on the native side. These tasks have been "lost", most likely because the user killed your app (which kills tasks on the native side without warning)

The rest of this section details [event listeners](#using-an-event-listener), [callbacks](#using-callbacks) and the [database](#using-the-database-to-track-tasks) in detail.

Expand Down Expand Up @@ -448,6 +449,8 @@ print('Taskid ${record.taskId} with task ${record.task} has '
You can interact with the `database` using `allRecords`, `allRecordsOlderThan`, `recordForId`,`deleteAllRecords`,
`deleteRecordWithId` etc. If you only want to track tasks in a specific [group](#grouping-tasks), call `trackTasksInGroup` instead.

If a user kills your app (e.g. by swiping it away in the app tray) then tasks that are running (natively) are killed, and no indication is given to your application. This cannot be avoided. To guard for this, upon app startup you can ask the downloader to reschedule missing tasks, i.e. tasks that show up as `enqueued` or `running` in the database, yet are not enqueued or running on the native side. Method `rescheduleMissingTasks` returns a record with two lists, `rescheduled` and `failed`. Together, those are the missing tasks, and the successfully rescheduled ones are in the `rescheduled` list. The time to do this is a few seconds after you have called `resumeFromBackground`, as that gives the downloader time to processes updates that may have happened while the app was suspended.

By default, the downloader uses a modified version of the [localstore](https://pub.dev/packages/localstore) package to store the `TaskRecord` and other objects. To use a different persistent storage solution, create a class that implements the [PersistentStorage](https://pub.dev/documentation/background_downloader/latest/background_downloader/PersistentStorage-class.html) interface, and initialize the downloader by calling `FileDownloader(persistentStorage: MyPersistentStorage())` as the first use of the `FileDownloader`.

As an alternative to LocalStore, use `SqlitePersistentStorage`, included in [background_downloader_sql](https://pub.dev/packages/background_downloader_sql), which supports SQLite storage and migration from Flutter Downloader.
Expand Down
28 changes: 28 additions & 0 deletions example/integration_test/database_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,32 @@ void main() {
final r2 = await database.recordForId(record2.taskId);
expect(r2, equals(record2));
});

test('rescheduleMissingTasks', () async {
expect(await FileDownloader().allTasks(), isEmpty);
// without task tracking activated, throws assertionError
expect(() async => await FileDownloader().rescheduleMissingTasks(),
throwsAssertionError);
await FileDownloader().trackTasks();
// test empty
final result = await FileDownloader().rescheduleMissingTasks();
expect(result.rescheduled, isEmpty);
expect(result.failed, isEmpty);
// add a record to the database that is not enqueued
await FileDownloader().database.updateRecord(record);
final result2 = await FileDownloader().rescheduleMissingTasks();
expect(result2.rescheduled.length, equals(1));
expect(result2.failed, isEmpty);
expect(result2.rescheduled.first.taskId, equals(task.taskId));
final allTasks = await FileDownloader().allTasks();
expect(allTasks.first.taskId, equals(task.taskId));
await Future.delayed(const Duration(seconds: 2));
expect(await FileDownloader().allTasks(), isEmpty);
// add a record to the database that is also enqueued
expect(await FileDownloader().enqueue(task2), isTrue);
expect(await FileDownloader().database.allRecords(), isNotEmpty);
final result3 = await FileDownloader().rescheduleMissingTasks();
expect(result3.rescheduled, isEmpty);
expect(result3.failed, isEmpty);
});
}
3 changes: 3 additions & 0 deletions lib/src/base_downloader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,9 @@ abstract base class BaseDownloader {
}
}

/// Return true if we are tracking tasks
bool get isTrackingTasks => trackedGroups.isNotEmpty;

/// Attempt to pause this [task]
///
/// Returns true if successful
Expand Down
47 changes: 47 additions & 0 deletions lib/src/file_downloader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,53 @@ interface class FileDownloader {
Future<void> resumeFromBackground() =>
_downloader.retrieveLocallyStoredData();

/// Reschedules tasks that are present in the database but missing from
/// the native task queue. Typically called on app start, 5s after establishing
/// the updates listener, calling [trackTasks] or [trackTasksInGroup] and
/// calling [resumeFromBackground].
///
/// This function retrieves all tasks from the database that are in enqueued
/// or running states and compares these tasks with the
/// list of tasks currently present in the native task queue.
///
/// For each task found only in the database, the function:
/// 1. Deletes the corresponding record from the database.
/// 2. Enqueues the task back into the native task queue.
///
/// Finally, the function returns two lists of Tasks in a record. The first
/// item [rescheduled] is the list of successfully re-enqueued tasks, the
/// second item [failed] is the list of tasks that failed to enqueue.
///
/// Throws assertion error if you are not currently tracking tasks, as that
/// makes this function a no-op that always returns empty lists.
Future<({List<Task> rescheduled, List<Task> failed})>
rescheduleMissingTasks() async {
assert(
_downloader.isTrackingTasks,
'rescheduleMissingTasks should only be called if you are tracking tasks. '
'Did you call trackTasks or trackTasksInGroup?');
final databaseTasks = (await database.allRecords())
.where((record) => const [
TaskStatus.enqueued,
TaskStatus.running,
].contains(record.status))
.map((record) => record.task)
.toSet();
final nativeTasks = Set<Task>.from(await FileDownloader().allTasks());
final missingTasks = databaseTasks.difference(nativeTasks);
final successfullyEnqueued = <Task>[];
final failedToEnqueue = <Task>[];
for (final task in missingTasks) {
await database.deleteRecordWithId(task.taskId);
if (await FileDownloader().enqueue(task)) {
successfullyEnqueued.add(task);
} else {
failedToEnqueue.add(task);
}
}
return (rescheduled: successfullyEnqueued, failed: failedToEnqueue);
}

/// Returns true if task can be resumed on pause
///
/// This future only completes once the task is running and has received
Expand Down

0 comments on commit 979785b

Please sign in to comment.