scheduler

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 16, 2026 License: MIT Imports: 5 Imported by: 0

README

kite-mcp-scheduler

Go Reference

Goroutine-safe scheduler for the algo2go ecosystem. Provides cron-style + interval ticker primitives with Clock injection for deterministic testing, lifecycle management (Start/Stop), and goroutine-leak sentinels.

Used by Sundeepg98/kite-mcp-server for IST-aligned background dispatch (morning briefings, EOD P&L, audit-log retention sweeps, Telegram dispatch loops, alert evaluation ticks).

Why a separate module?

Background scheduling is an orthogonal infrastructure primitive — usable by any algo2go project (broker dashboards, monitoring, broker-adapter health checks, future trading bots) independent of kite-mcp-server. Centralizing as a module:

  • Lets the Clock-injection pattern + leak sentinels be shared consistently across consumers
  • Encourages deterministic-test discipline (Clock injection + goroutine-leak detection together)
  • Keeps the dep-graph weight minimal for users who only need scheduling

Stability promise

v0.x — unstable. Public types may break between minor versions. Pin v0.1.0 deliberately. v1.0 ships only after the public API is reviewed for stability.

Install

go get github.com/algo2go/kite-mcp-scheduler@v0.1.0

Dependencies

  • github.com/algo2go/kite-mcp-isttz — IST timezone wrapper for market-hours-aligned ticks
  • go.uber.org/goleak — goroutine-leak detection in tests
  • github.com/stretchr/testify — assertions

Public API (scheduler.go)

  • Scheduler — lifecycle-managed scheduler with Start/Stop semantics
  • Clock interface — Now() + tick scheduling; for deterministic tests inject MockClock
  • Tick registration helpers: every-N-minutes, daily-at-IST, weekday-at-IST patterns

See pkg.go.dev for full type docs.

Reference consumer

Sundeepg98/kite-mcp-server — wires Scheduler in app/providers/scheduler.go and consumes via:

  • Morning briefing dispatch (9:00 IST weekdays)
  • EOD P&L snapshot (15:35 IST weekdays)
  • Alert evaluation tick (every 30s during market hours)
  • Audit-log retention sweep (3:00 IST daily)

License

MIT — see LICENSE.

Authors

Original design: Sundeepg98 (Zerodha Tech). Multi-module promotion (2026-05-10): algo2go contributors.

Documentation

Overview

Package scheduler provides a simple IST-aware task scheduler that runs named tasks at specific times on trading days (Monday–Friday).

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsMarketHoliday

func IsMarketHoliday(t time.Time) bool

IsMarketHoliday returns true if the given time falls on an NSE trading holiday.

func IsTradingDay

func IsTradingDay(t time.Time) bool

IsTradingDay returns true if the given time is a weekday and not a market holiday.

func IsWeekend

func IsWeekend(t time.Time) bool

IsWeekend returns true if the given time falls on Saturday or Sunday in IST.

func MarketStatus

func MarketStatus(t time.Time) string

MarketStatus returns the current market status based on IST time. Possible values: "pre_open", "open", "closing_session", "closed", "closed_weekend", "closed_holiday".

func NowIST

func NowIST() time.Time

NowIST returns the current time in IST.

func TodayIST

func TodayIST() string

TodayIST returns today's date string in IST (format "2006-01-02").

Types

type Clock

type Clock func() time.Time

Clock returns the current time. Defaults to time.Now. Override in tests via SetClock to control time.

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler checks every minute whether any registered task should fire. Tasks only run on weekdays (Mon-Fri) and at most once per calendar day.

func New

func New(logger *slog.Logger) *Scheduler

New creates a new Scheduler.

func (*Scheduler) Add

func (s *Scheduler) Add(task Task)

Add registers a task. Must be called before Start.

func (*Scheduler) ListTaskNames

func (s *Scheduler) ListTaskNames() []string

ListTaskNames returns the Name field of every task currently held by the scheduler in registration order. Intended for tests, admin tooling, and log diagnostics ("what's the active schedule?").

Snapshot: safe for concurrent use; the returned slice is a copy.

func (*Scheduler) RegisterTaskProvider

func (s *Scheduler) RegisterTaskProvider(p TaskProvider)

RegisterTaskProvider installs a TaskProvider. Safe to call before Start; panic-safe against nil (nil providers are silently dropped so a plugin that returns a typed-nil from a feature-flag path doesn't crash scheduler init).

Registering the same provider twice is allowed but discouraged — both registrations will fire at Start, producing duplicate tasks whose Name() collision will cause the tick loop to treat them as "already ran today" after the first fires. Use a single provider instance per logical source of tasks.

func (*Scheduler) SetClock

func (s *Scheduler) SetClock(c Clock)

SetClock overrides the time source (for testing). Safe to call concurrently with a running scheduler; tick() reads the clock under s.mu.

func (*Scheduler) SetTickInterval

func (s *Scheduler) SetTickInterval(d time.Duration)

SetTickInterval overrides the loop tick interval (for testing). Must be called before Start — the loop caches the interval at launch and won't observe later changes.

func (*Scheduler) Start

func (s *Scheduler) Start()

Start launches a background goroutine that ticks every minute.

Before the goroutine launches, every registered TaskProvider is consulted via Tasks() and its returned tasks are appended to the scheduler's task list. This is the single collection point for plugin-contributed schedules — Tasks() is NOT called again until the scheduler is stopped and a new one is constructed.

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop signals the scheduler goroutine to exit and waits for it to finish. Safe to call multiple times (select guards the channel close, loopDone reads are idempotent on a closed channel). Safe to call without Start: loopDone is initialised in New and only closed by loop — the bounded wait below protects against "Stop before Start" by returning once a small window passes with no signal.

type Task

type Task struct {
	Name   string // unique identifier for dedup tracking
	Hour   int    // IST hour (0-23)
	Minute int    // IST minute (0-59)
	Fn     func() // the work to perform
}

Task describes a named function that should run once per trading day at a specific IST time.

type TaskProvider

type TaskProvider interface {
	// Name is a stable identifier for logs and duplicate detection.
	// Must be unique across registered providers; a second provider
	// with the same Name replaces the first (last-wins). Use snake_case.
	Name() string

	// Tasks returns the tasks this provider wishes to contribute.
	// Called exactly once at Start(). A provider that wishes to
	// contribute no tasks (e.g. feature-flagged off) should return a
	// nil or empty slice — the provider registration itself is not
	// an error.
	//
	// The returned slice is copied into the scheduler's task list
	// before Start launches the tick goroutine, so callers need not
	// worry about the scheduler retaining or mutating the slice.
	Tasks() []Task
}

TaskProvider is the plugin extension point for Scheduler.

A TaskProvider contributes zero or more Tasks at startup. Unlike the pre-existing Add(Task) API — which expects callers to know their task definitions at construction time — a provider is consulted once at Start(): every registered provider has its Tasks() method invoked, and the returned tasks are appended to the scheduler's task list before the tick loop begins.

Design rationale:

  • Plugins cannot always statically list their tasks at wire-up time. A billing plugin might only emit a "collect_usage_metrics" task if Stripe is configured; a compliance plugin might emit per-exchange holiday-reminder tasks that depend on an instruments manifest loaded after construction. Deferring task enumeration until Start lets providers inspect runtime state that isn't ready at New().

  • Collecting once at Start (as opposed to per-tick) preserves the existing "tasks are fixed once scheduler runs" invariant, which other callers depend on. Dynamic task registration would require re-examining the dedup map (lastRun) on every tick — a tradeoff we explicitly rejected because it makes "did this task run today?" harder to reason about.

  • The TaskProvider interface is intentionally narrow. It carries no lifecycle, config, or teardown hooks. Providers that need their own lifecycle should wire that through their enclosing module (e.g. the plugin's own Init/Shutdown); the scheduler cares only about "give me your tasks".

Typical usage in app wiring:

sched := scheduler.New(logger)
sched.RegisterTaskProvider(myplugin.NewScheduleProvider(deps))
// ... sched.Add(Task{...}) for built-in tasks ...
sched.Start()

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL