fix(learn): the instance asks LanguageTool for its languages again a minute after it didn't answer
Switching the sidecar on learn.halkyon.eu's server recreated the server beside LanguageTool, whose start takes a minute: the server's first look at the languages failed, and it looked again only an hour later, as after one that worked. Until then instance.info() named no languages, and an app that asked meanwhile keeps that answer, so its editors check nothing until it starts anew. A look LanguageTool doesn't answer is now tried again a minute later, by a timer, until it answers, the languages known before kept meanwhile; one that worked still counts for an hour. The server's tests take fake_async (^1.3.3, as Learn's app, already in the lock) for the timer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
db7f6c0ab2
commit
19dd4d9c4e
5 changed files with 66 additions and 4 deletions
File diff suppressed because one or more lines are too long
|
|
@ -42,11 +42,17 @@ class GrammarChecking {
|
||||||
List<String>? _codes;
|
List<String>? _codes;
|
||||||
DateTime? _lookedAt;
|
DateTime? _lookedAt;
|
||||||
Future<void>? _looking;
|
Future<void>? _looking;
|
||||||
|
Timer? _again;
|
||||||
final _asked = HashMap<UuidValue, Queue<DateTime>>();
|
final _asked = HashMap<UuidValue, Queue<DateTime>>();
|
||||||
|
|
||||||
/// How long a look at the languages counts.
|
/// How long a look at the languages counts.
|
||||||
static const _fresh = Duration(hours: 1);
|
static const _fresh = Duration(hours: 1);
|
||||||
|
|
||||||
|
/// How soon a look LanguageTool didn't answer is tried again: it takes
|
||||||
|
/// a minute to start, and a server started beside it looks at once, and
|
||||||
|
/// apps keep the languages they were told.
|
||||||
|
static const _retry = Duration(minutes: 1);
|
||||||
|
|
||||||
/// The languages checked here (`de`, `ja`); null without a LanguageTool,
|
/// The languages checked here (`de`, `ja`); null without a LanguageTool,
|
||||||
/// or while it hasn't answered. Looks again in the background once the
|
/// or while it hasn't answered. Looks again in the background once the
|
||||||
/// last look is an hour old, and never waits.
|
/// last look is an hour old, and never waits.
|
||||||
|
|
@ -62,12 +68,14 @@ class GrammarChecking {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asks LanguageTool which languages it checks; one look at a time. A
|
/// Asks LanguageTool which languages it checks; one look at a time. A
|
||||||
/// failed look forgets none it knew.
|
/// failed look forgets none it knew, and looks again after [_retry],
|
||||||
|
/// until LanguageTool answers.
|
||||||
Future<void> look() => _looking ??= () async {
|
Future<void> look() => _looking ??= () async {
|
||||||
|
_again?.cancel();
|
||||||
try {
|
try {
|
||||||
_codes = await languageTool?.languages() ?? _codes;
|
_codes = await languageTool?.languages() ?? _codes;
|
||||||
} on LanguageToolFailed {
|
} on LanguageToolFailed {
|
||||||
// Unknown until the next look.
|
_again = Timer(_retry, () => unawaited(look()));
|
||||||
} finally {
|
} finally {
|
||||||
_lookedAt = clock.now();
|
_lookedAt = clock.now();
|
||||||
_looking = null;
|
_looking = null;
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ dependencies:
|
||||||
serverpod_auth_idp_server: 4.0.4
|
serverpod_auth_idp_server: 4.0.4
|
||||||
serverpod_offline_sync_server: 0.0.10
|
serverpod_offline_sync_server: 0.0.10
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
|
# Time in tests that wait on timers (looking again for LanguageTool's languages).
|
||||||
|
fake_async: ^1.3.3
|
||||||
serverpod_test: 4.0.4
|
serverpod_test: 4.0.4
|
||||||
test: ^1.25.5
|
test: ^1.25.5
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:clock/clock.dart';
|
import 'package:clock/clock.dart';
|
||||||
|
import 'package:fake_async/fake_async.dart';
|
||||||
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
import 'package:halkyon_learn_server/src/generated/protocol.dart';
|
||||||
import 'package:halkyon_learn_server/src/grammar/grammar_checking.dart';
|
import 'package:halkyon_learn_server/src/grammar/grammar_checking.dart';
|
||||||
import 'package:halkyon_learn_server/src/grammar/language_tool.dart';
|
import 'package:halkyon_learn_server/src/grammar/language_tool.dart';
|
||||||
|
|
@ -285,6 +286,51 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('looks again a minute after LanguageTool failed to answer, as it '
|
||||||
|
'does while starting beside the server, until it answers', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
final fake = _FakeLanguageTool()..fails = true;
|
||||||
|
final checking = GrammarChecking(languageTool: fake);
|
||||||
|
unawaited(checking.look());
|
||||||
|
async.flushMicrotasks();
|
||||||
|
expect(checking.languages, isNull);
|
||||||
|
expect(fake.looks, 1);
|
||||||
|
|
||||||
|
async.elapse(const Duration(seconds: 61));
|
||||||
|
expect(fake.looks, 2, reason: 'still failing: again a minute on');
|
||||||
|
|
||||||
|
fake.fails = false;
|
||||||
|
async.elapse(const Duration(seconds: 61));
|
||||||
|
expect(fake.looks, 3);
|
||||||
|
expect(checking.languages, ['de', 'en', 'ja']);
|
||||||
|
|
||||||
|
async.elapse(const Duration(minutes: 30));
|
||||||
|
expect(fake.looks, 3, reason: 'answered: nothing until an hour on');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the languages it knows when a later look fails, and looks '
|
||||||
|
'again a minute after', () {
|
||||||
|
fakeAsync((async) {
|
||||||
|
final fake = _FakeLanguageTool();
|
||||||
|
final checking = GrammarChecking(languageTool: fake);
|
||||||
|
unawaited(checking.look());
|
||||||
|
async.elapse(const Duration(minutes: 61));
|
||||||
|
fake
|
||||||
|
..fails = true
|
||||||
|
..codes = ['de-DE', 'en-US', 'ja-JP', 'el-GR'];
|
||||||
|
expect(checking.languages, ['de', 'en', 'ja']);
|
||||||
|
async.flushMicrotasks();
|
||||||
|
expect(fake.looks, 2);
|
||||||
|
expect(checking.languages, ['de', 'en', 'ja']);
|
||||||
|
|
||||||
|
fake.fails = false;
|
||||||
|
async.elapse(const Duration(seconds: 61));
|
||||||
|
expect(fake.looks, 3);
|
||||||
|
expect(checking.languages, ['de', 'el', 'en', 'ja']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('checks in the code the language becomes, nothing for an empty '
|
test('checks in the code the language becomes, nothing for an empty '
|
||||||
'text', () async {
|
'text', () async {
|
||||||
final fake = _FakeLanguageTool();
|
final fake = _FakeLanguageTool();
|
||||||
|
|
@ -374,11 +420,15 @@ class _FakeLanguageTool implements LanguageTool {
|
||||||
bool fails = false;
|
bool fails = false;
|
||||||
final checked = <(String, String)>[];
|
final checked = <(String, String)>[];
|
||||||
|
|
||||||
|
/// How often it was asked for its languages.
|
||||||
|
int looks = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uri get address => Uri.parse('http://languagetool:8081/');
|
Uri get address => Uri.parse('http://languagetool:8081/');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> languages() async {
|
Future<List<String>> languages() async {
|
||||||
|
looks++;
|
||||||
if (fails) {
|
if (fails) {
|
||||||
throw const LanguageToolFailed('down');
|
throw const LanguageToolFailed('down');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,9 @@ directly, a checker sees what people write while they write it.
|
||||||
into `deploy/languagetool/fasttext`) makes the recognised language of a note far more
|
into `deploy/languagetool/fasttext`) makes the recognised language of a note far more
|
||||||
reliable than LanguageTool's own guess; it costs 135 MB of memory.
|
reliable than LanguageTool's own guess; it costs 135 MB of memory.
|
||||||
- **Languages**: the instance asks its LanguageTool which it checks as it starts and an
|
- **Languages**: the instance asks its LanguageTool which it checks as it starts and an
|
||||||
hour after, and `instance.info()` names them (`grammarLanguages`, primary subtags), so
|
hour after, and a minute after a look it didn't answer (added 26 Oct, after the first
|
||||||
|
deploy: a server started beside it looks before LanguageTool answers, and apps keep the
|
||||||
|
languages they were told), and `instance.info()` names them (`grammarLanguages`, primary subtags), so
|
||||||
the app offers checks only where they work. A tag maps to LanguageTool's code
|
the app offers checks only where they work. A tag maps to LanguageTool's code
|
||||||
(`languageToolCode`): `en` and `de` to `en-US` and `de-DE`, since the bare codes check no
|
(`languageToolCode`): `en` and `de` to `en-US` and `de-DE`, since the bare codes check no
|
||||||
spelling; traditional Chinese to none, as LanguageTool only knows simplified; an empty
|
spelling; traditional Chinese to none, as LanguageTool only knows simplified; an empty
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue