Skip to content

feat(server): MentorReflectionScheduler with per-user cadence and time of day #1258

@FelixTJDietrich

Description

@FelixTJDietrich

Part of #1204.

What ships

A MentorReflectionScheduler that runs proactive reflection mentor sessions on a per-user cadence. The scheduler reads each user's preference row, resolves the next due time in the user's timezone, and triggers a SLACK_DM-surface mentor session through the same orchestrator + adapter the on-demand entry points use.

Cadence options:

Option Trigger
OFF No proactive reflection; user can still open mentor on demand.
WEEKLY (default) One reflection per week. Default: Monday 09:00 in the user's resolved timezone. User configurable time_of_day + weekday.
END_OF_SPRINT One reflection per sprint, fired at sprint-end + 1 day at the user's configured time_of_day. Consumes the workspace's sprint calendar.
DAILY One reflection per weekday at the user's configured time_of_day. Tightened min-interval enforced by #1259.

A new user_reflection_preference(user_id, workspace_id, cadence, weekday, time_of_day, timezone, updated_at) table stores the configuration. Default for a new user in a workspace is WEEKLY, Monday, 09:00, with timezone resolved from the identity-link OIDC zoneinfo claim (#1238). A user whose timezone cannot be resolved is treated as OFF until they configure it via #1262.

The scheduler is a @Scheduled Spring task running every 5 minutes. Each tick queries due users (next_due_at <= now()) with a SKIP LOCKED row lock so multiple instances do not double-fire. Each due user enqueues one reflection job through the shared notification budget from #1260; the job composes the three-phase reflection template (#1261) and the DM safety rails (#1259) before sending.

Why

The scheduled cadence is the SDT-aligned proactive intervention the SRL literature recommends — the system asks the right question on a predictable rhythm the learner chose. Without per-user cadence + timezone resolution, the alternative is either spam (one cadence for all) or silence (no proactive reflection). The default of weekly Monday 09:00 matches the formative-feedback literature's "weekly retrospective" finding and is the cadence pilot workspaces converged on; the option set is small enough that the configuration UI from #1262 is one screen.

Acceptance criteria

  • user_reflection_preference table exists via Liquibase changeset with the columns above, a unique (user_id, workspace_id) index, and a CHECK constraint on cadence enum values
  • On user-workspace creation a default row is inserted: cadence=WEEKLY, weekday=MONDAY, time_of_day=09:00, timezone=<resolved from OIDC or NULL>
  • The MentorReflectionScheduler runs at a 5-minute cadence and uses a SKIP LOCKED query to fetch due users; an integration test runs the scheduler twice concurrently and asserts no user fires twice in one tick
  • Cadence OFF is honored — no DMs fired; an integration test seeds three users with OFF / WEEKLY / DAILY and asserts only the latter two fire in a one-week window
  • Timezone resolution falls back to "treat as OFF" when unknown; a structured INFO log carries workspace_id + user_id and the absent-tz reason
  • END_OF_SPRINT consumes the workspace sprint calendar; the next-due-at is recomputed when sprint dates change (an integration test moves sprint-end and asserts the next due time tracks)
  • The scheduler enqueues through the shared notification budget from feat(server): shared notification budget across mentor and finding delivery #1260; rejected enqueues (budget exhausted, kill switch) produce a metric increment and a structured INFO, not an exception
  • An integration test asserts a DAILY user with a 17 h gap since last reflection is rejected by the tighter 18 h min-interval from feat(server): DM safety rails for proactive mentor sends #1259

Tests to write

  • MentorReflectionSchedulerIT — per-cadence happy path + skip-locked concurrency.
  • UserReflectionPreferenceDefaultsTest — workspace-membership creation inserts the default row.
  • TimezoneResolutionFallbackTest — absent OIDC zoneinfo claim → treat-as-OFF behavior.
  • EndOfSprintCadenceIT — sprint-end shift propagates to next_due_at.

Implementation notes

  • The scheduler does not own DM sending; it enqueues. The DM safety rails in feat(server): DM safety rails for proactive mentor sends #1259 and the shared budget in feat(server): shared notification budget across mentor and finding delivery #1260 sit between the scheduler and the adapter. Splitting the concerns this way keeps the scheduler stateless about safety and makes the budget the single accounting surface.
  • The next_due_at computation is the load-bearing piece: for WEEKLY it's the next occurrence of weekday at time_of_day in timezone after the previous send; for END_OF_SPRINT it's the next sprint-end + 1 day at time_of_day; for DAILY it's the next weekday at time_of_day. A pure function with property tests is cheaper than a stateful one.
  • The scheduler runs in the application-server JVM, not in a separate worker; the SKIP LOCKED row lock is the multi-instance coordination primitive. Spring Modulith Event Publication Registry from feat(server): Modulith Event Publication Registry as outbound outbox #1209 is not on the proactive path — it's the outbound-delivery outbox, downstream of the budget.
  • Workspaces without a sprint calendar treat END_OF_SPRINT as WEEKLY Monday with a structured WARN; the configuration UI surfaces this fallback to avoid silent surprise.
  • The workspace admin cannot set a per-user cadence on behalf of users; only the user (via feat(webapp,server): cadence preferences UI in webapp and Slack modal #1262) writes their preference row. The integration audit log captures preference changes for SAR compliance.

Dependencies

Depends on #1257. Depends on #1238. Blocks #1259. Blocks #1261. Blocks #1262.

Metadata

Metadata

Assignees

No one assigned

    Labels

    application-serverSpring Boot server: APIs, business logic, databaseenhancementImprovement to existing functionalityfeatureNew feature or enhancementpriority:highAddress this sprint - Significant impact

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions