This commit is contained in:
Nathan Anderson 2026-06-05 18:26:27 -05:00
parent 76c73a30b4
commit 46cc822586
4 changed files with 31 additions and 0 deletions

View File

@ -37,3 +37,4 @@ MARKET_HISTORY_RETENTION_DAYS=5
# Public API URL baked into Flutter web builds (see scripts/build-production-web.sh).
# Use same origin as the site when HAProxy proxies /v1 and /hubs on :443.
API_BASE_URL=https://cyberhybridhub.com
QUESTION_DISPATCHER_DEBUG_LOGGING_ENABLED=false

View File

@ -240,6 +240,7 @@ Future<void> main() async {
questionService: questionService,
questionPipeline: questionPipeline,
preferencesDb: questionSourcePreferencesDb,
debugLoggingEnabled: env.questionDispatcherDebugLoggingEnabled,
);
questionPipeline.attachQuestionDispatcher(questionDispatcher);
QuestionBackgroundWorker? backgroundWorker;

View File

@ -21,6 +21,7 @@ class ServerEnv {
required this.adminFirebaseUids,
required this.marketHistory,
required this.alpaca,
required this.questionDispatcherDebugLoggingEnabled,
});
final String databaseUrl;
@ -29,6 +30,7 @@ class ServerEnv {
final bool questionWorkerEnabled;
final int questionWorkerIntervalSeconds;
final bool questionPipelineTestMode;
final bool questionDispatcherDebugLoggingEnabled;
/// When true, prospective answers earn closeness bonus points and wrong
/// direction scores `-2` instead of `-1`. Default false (`+1`/`-1` only).
@ -115,6 +117,10 @@ class ServerEnv {
.map((String value) => value.trim())
.where((String value) => value.isNotEmpty)
.toSet();
final bool questionDispatcherDebugLoggingEnabled =
(env['QUESTION_DISPATCHER_DEBUG_LOGGING_ENABLED'] ?? 'false')
.toLowerCase() ==
'true';
final Map<String, String> stringEnv =
Map<String, String>.from(Platform.environment);
const List<String> marketHistoryKeys = <String>[
@ -158,6 +164,7 @@ class ServerEnv {
adminFirebaseUids: adminFirebaseUids,
marketHistory: marketHistory,
alpaca: alpaca,
questionDispatcherDebugLoggingEnabled: questionDispatcherDebugLoggingEnabled,
);
loaded.assertConsistent();
return loaded;

View File

@ -12,6 +12,7 @@ class QuestionDispatcher {
required QuestionService questionService,
required QuestionPipeline questionPipeline,
required QuestionSourcePreferencesDb preferencesDb,
this.debugLoggingEnabled = false,
this.maxQueuedQuestions = 3,
}) : _questionsDb = questionsDb,
_questionService = questionService,
@ -22,26 +23,39 @@ class QuestionDispatcher {
final QuestionService _questionService;
final QuestionPipeline _questionPipeline;
final QuestionSourcePreferencesDb _preferencesDb;
final bool debugLoggingEnabled;
final int maxQueuedQuestions;
void _log(String message) {
if (debugLoggingEnabled) {
// ignore: avoid_print
print('[QuestionDispatcher] $message');
}
}
/// Maximum maintenance passes before giving up on an empty queue.
static const int _maxQueueAttempts = 6;
/// Ensures at least one enabled-source question exists; returns mix-ordered head.
Future<Map<String, dynamic>?> dispatchNextQuestion(String firebaseUid) async {
_log('dispatchNextQuestion($firebaseUid)');
await _questionsDb.ensureUserExists(firebaseUid);
var rows = await _listEnabledUnansweredOrdered(firebaseUid);
_log('Initial unanswered for mix: ${rows.length} rows');
for (var attempt = 0;
rows.isEmpty && attempt < _maxQueueAttempts;
attempt++) {
_log('Queue empty, running maintenance (attempt $attempt)');
await _runQueueMaintenance(firebaseUid);
rows = await _listEnabledUnansweredOrdered(firebaseUid);
}
if (rows.isEmpty) {
_log('Still empty after maintenance, returning null');
return null;
}
_log('Returning first row out of ${rows.length}');
return _toClientPayload(rows.first, rows.length);
}
@ -84,8 +98,10 @@ class QuestionDispatcher {
required String firebaseUid,
required Map<String, dynamic> answeredQuestion,
}) async {
_log('nextQuestionAfterAnswer($firebaseUid)');
var rows = await _listEnabledUnansweredOrdered(firebaseUid);
if (rows.isEmpty) {
_log('Queue empty, falling back to dispatchNextQuestion');
return dispatchNextQuestion(firebaseUid);
}
@ -97,6 +113,7 @@ class QuestionDispatcher {
final Set<String> allowed = _enabledSourceIds(prefs)
.where((String id) => (mix[id] ?? 0) > 0)
.toSet();
_log('nextQuestionAfterAnswer: mix=$mix, allowed=$allowed, delivered=$delivered');
final Map<String, dynamic>? row = await _questionsDb.findHeadUnansweredForMix(
firebaseUid,
effectiveMixPercent: mix,
@ -156,10 +173,12 @@ class QuestionDispatcher {
}
Future<bool> _enqueueOneFromMix(String firebaseUid) async {
_log('_enqueueOneFromMix($firebaseUid)');
final List<ResolvedQuestionSourcePreference> prefs =
await _preferencesDb.getResolved(firebaseUid);
final Map<String, int> mix = QuestionSourceMix.effectiveMixFromResolved(prefs);
if (mix.isEmpty) {
_log('_enqueueOneFromMix: mix is empty');
return false;
}
@ -169,6 +188,7 @@ class QuestionDispatcher {
effectiveMixPercent: mix,
deliveredCounts: delivered,
);
_log('_enqueueOneFromMix: picked primary=$primary (mix=$mix, delivered=$delivered)');
final List<String> order = _sourceTryOrderForEnqueue(
prefs: prefs,
primarySourceId: primary,
@ -201,8 +221,10 @@ class QuestionDispatcher {
.where((String id) => (mix[id] ?? 0) > 0)
.toSet();
if (allowed.isEmpty) {
_log('_listEnabledUnansweredOrdered: allowed is empty');
return <Map<String, dynamic>>[];
}
_log('_listEnabledUnansweredOrdered: fetching for allowed=$allowed');
return _questionsDb.listUnansweredOrderedForMix(
firebaseUid,
effectiveMixPercent: mix,