diff --git a/server/.env.example b/server/.env.example index 49c3749..393430e 100644 --- a/server/.env.example +++ b/server/.env.example @@ -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 diff --git a/server/bin/server.dart b/server/bin/server.dart index e13b8d6..0e73b67 100644 --- a/server/bin/server.dart +++ b/server/bin/server.dart @@ -240,6 +240,7 @@ Future main() async { questionService: questionService, questionPipeline: questionPipeline, preferencesDb: questionSourcePreferencesDb, + debugLoggingEnabled: env.questionDispatcherDebugLoggingEnabled, ); questionPipeline.attachQuestionDispatcher(questionDispatcher); QuestionBackgroundWorker? backgroundWorker; diff --git a/server/lib/env.dart b/server/lib/env.dart index adab221..16458f6 100644 --- a/server/lib/env.dart +++ b/server/lib/env.dart @@ -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 stringEnv = Map.from(Platform.environment); const List marketHistoryKeys = [ @@ -158,6 +164,7 @@ class ServerEnv { adminFirebaseUids: adminFirebaseUids, marketHistory: marketHistory, alpaca: alpaca, + questionDispatcherDebugLoggingEnabled: questionDispatcherDebugLoggingEnabled, ); loaded.assertConsistent(); return loaded; diff --git a/server/lib/question_dispatcher.dart b/server/lib/question_dispatcher.dart index cc6c7a8..4486f8d 100644 --- a/server/lib/question_dispatcher.dart +++ b/server/lib/question_dispatcher.dart @@ -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?> 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 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 allowed = _enabledSourceIds(prefs) .where((String id) => (mix[id] ?? 0) > 0) .toSet(); + _log('nextQuestionAfterAnswer: mix=$mix, allowed=$allowed, delivered=$delivered'); final Map? row = await _questionsDb.findHeadUnansweredForMix( firebaseUid, effectiveMixPercent: mix, @@ -156,10 +173,12 @@ class QuestionDispatcher { } Future _enqueueOneFromMix(String firebaseUid) async { + _log('_enqueueOneFromMix($firebaseUid)'); final List prefs = await _preferencesDb.getResolved(firebaseUid); final Map 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 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 >[]; } + _log('_listEnabledUnansweredOrdered: fetching for allowed=$allowed'); return _questionsDb.listUnansweredOrderedForMix( firebaseUid, effectiveMixPercent: mix,