Background work
Answer the request now and do the slow work after. Use another thread for CPU work. Run a job on a timer.
What you need
- A Dox app.
What Dox gives you
Dox has no job queue. It has three building blocks from Dart itself:
| You want | Use | Runs where |
|---|---|---|
| I/O work after the response (send an email, call an API, write rows) | a Future you do not await |
the same isolate, between requests |
| CPU work (resize an image, parse a big file, compute) | Isolate.run() |
a new thread |
| work every N minutes (cleanup, sync) | Timer.periodic in a service |
one isolate of your choice |
None of them survive a restart. See When you need a real queue.
1. Work after the response
Start the future and return. unawaited tells the linter you did this on purpose.
An error in the future goes to the errorHandler of AppConfig. The isolate keeps running.
import 'dart:async';
import 'dart:isolate';
import 'package:dox_core/dox_core.dart';
class ReportController {
/// POST /api/reports
/// Answers at once. The work runs after the response, in this isolate.
Map<String, String> store(DoxRequest req) {
String id = DateTime.now().microsecondsSinceEpoch.toString();
unawaited(_build(id));
return <String, String>{'id': id, 'status': 'queued'};
}
/// I/O work (database, HTTP calls, email) is fine here. It shares the
/// event loop with the requests, so it must not block the CPU.
Future<void> _build(String id) async {
await Future<void>.delayed(const Duration(seconds: 2));
print('report $id is ready');
}
/// GET /api/primes/{limit}
/// CPU work runs on another thread with `Isolate.run`.
/// The request waits for the result, but other requests are not blocked.
Future<Map<String, int>> primes(int limit) async {
int count = await Isolate.run(() => countPrimes(limit));
return <String, int>{'limit': limit, 'primes': count};
}
}
/// Must be a top level or static function with plain arguments.
/// The new isolate has no Dox, no database and no services.
int countPrimes(int limit) {
List<bool> sieve = List<bool>.filled(limit + 1, true);
int count = 0;
for (int i = 2; i <= limit; i++) {
if (!sieve[i]) continue;
count++;
for (int j = i * i; j <= limit; j += i) {
sieve[j] = false;
}
}
return count;
}
ReportController reportController = ReportController();
Do not block the event loop
_build runs on the same thread as the HTTP requests of this isolate.
await on the database or on HTTP is fine. A long loop is not. Every request on this isolate waits for it.
Put a long loop in Isolate.run.
2. CPU work on another thread
Isolate.run starts a new isolate, runs the function, and sends the result back.
The function only gets what you pass in. It has no Dox(), no models and no Storage().
So pass plain data in (an int, a String, bytes), get plain data out, and save the result in the request isolate.
Both patterns are in the controller above: primes waits for the result. To not wait, combine them:
3. Work on a timer
setup() of a service runs in every isolate. Dox starts one per CPU core.
So start the timer on isolate 1 only. Otherwise 8 isolates run the same cleanup.
import 'dart:async';
import 'package:dox_core/dox_core.dart';
import 'package:my_app/app/models/user/user.model.dart';
/// Work on a timer. `setup()` runs in every isolate, so the timer is
/// started on isolate 1 only. Otherwise 8 isolates do the same job.
class CleanupService implements DoxService {
@override
void setup() {
if (Dox().isolateId != 1) return;
Timer.periodic(const Duration(hours: 1), _run);
}
Future<void> _run(Timer timer) async {
DateTime limit = DateTime.now().subtract(const Duration(days: 30));
List<User> old = await User()
.withTrash()
.where('deleted_at', '<', limit)
.get();
for (User user in old) {
await user.forceDelete();
}
print('cleanup: removed ${old.length} users');
}
}
List<DoxService> services = <DoxService>[
ORMService(),
CleanupService(), // after ORMService, so the database is ready
];
When you run more than one process (two machines), each process has an isolate 1. Then the job runs twice. Use a database lock, or run the job from one place only.
4. Add the routes
Route.post('/reports', reportController.store);
Route.get('/primes/{limit}', reportController.primes);
Check it
The answer comes at once:
Two seconds later, the server prints report 1792677668393002 is ready.
When you need a real queue
The work above is lost when the process stops, and it does not retry.
When that is not OK (payments, emails that must arrive), save the job as a row in a table first.
Then a Timer.periodic service, or a second small Dart program, reads the table, does the work, and marks the row as done.
A failed row stays in the table, so you can retry it. This is a queue with one extra table and no new service.
See also
- Isolate for what each isolate shares, and what it does not
- Services
- Custom errors for the
errorHandlerthat gets the errors of a future nobody awaits