Skip to content
Open
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ linters:
- demo-app

rules:
- path: "(scheduler|internal|session|auth|application|doc)/*"
- path: "(session|auth)/*"
linters:
- revive

Expand Down
9 changes: 6 additions & 3 deletions application/application.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package application provides core application lifecycle management.
package application

import (
Expand Down Expand Up @@ -37,16 +38,16 @@ type Application struct {
services map[string]Runner
healthcheckers map[string]Healthchecker
databases map[string]*database.Database
health *ApplicationHealth
health *Health
}

// New creates and returns a new Application instance.
func New() *Application {
return &Application{services: make(map[string]Runner), healthcheckers: make(map[string]Healthchecker), databases: make(map[string]*database.Database), health: NewApplicationHealth()}
return &Application{services: make(map[string]Runner), healthcheckers: make(map[string]Healthchecker), databases: make(map[string]*database.Database), health: NewHealth()}
}

// Health returns the current health status of the application.
func (a *Application) Health(ctx context.Context) *ApplicationHealth {
func (a *Application) Health(ctx context.Context) *Health {
for hcName, hc := range a.healthcheckers {
a.health.SetServiceData(hcName, hc.Healthcheck(ctx))
}
Expand All @@ -58,6 +59,7 @@ func (a *Application) OnStart(task Runner, config StartupTaskConfig) {
a.startupTasks = append(a.startupTasks, startupTask{task, config})
}

// OnStartFunc registers a startup task using a RunnerFunc.
func (a *Application) OnStartFunc(task RunnerFunc, config StartupTaskConfig) {
a.startupTasks = append(a.startupTasks, startupTask{task, config})
}
Expand All @@ -84,6 +86,7 @@ func (a *Application) RegisterService(serviceName string, service Runner) {
}
}

// RegisterDomain registers a domain repository in the specified database.
func (a *Application) RegisterDomain(name, dbName string, domain Domain) {
if dbName != "" {
repository := domain.GetRepository()
Expand Down
1 change: 1 addition & 0 deletions application/domain.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package application

// Domain describes a domain module that exposes its repository.
type Domain interface {
GetRepository() any
}
31 changes: 21 additions & 10 deletions application/health.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ import (
"time"
)

// ServiceStatus represents the lifecycle state of a service.
type ServiceStatus string

const (
// ServiceStatusNotStarted indicates service has not started yet.
ServiceStatusNotStarted ServiceStatus = "NOT_STARTED"
ServiceStatusStarted ServiceStatus = "STARTED"
ServiceStatusError ServiceStatus = "ERROR"
// ServiceStatusStarted indicates service is currently running.
ServiceStatusStarted ServiceStatus = "STARTED"
// ServiceStatusError indicates service finished with an error.
ServiceStatusError ServiceStatus = "ERROR"
)

// ServiceHealth contains health information for a single service.
type ServiceHealth struct {
Status ServiceStatus `json:"status"`
StartedAt *time.Time `json:"startedAt"`
Expand All @@ -21,16 +26,19 @@ type ServiceHealth struct {
Data any `json:"data,omitempty"`
}

type ApplicationHealth struct {
// Health contains overall application health and service states.
type Health struct {
StartedAt time.Time `json:"startedAt"`
Services map[string]*ServiceHealth `json:"services"`
}

func NewApplicationHealth() *ApplicationHealth {
return &ApplicationHealth{Services: make(map[string]*ServiceHealth)}
// NewHealth creates an ApplicationHealth with initialized storage.
func NewHealth() *Health {
return &Health{Services: make(map[string]*ServiceHealth)}
}

func (h *ApplicationHealth) StartService(serviceName string) {
// StartService marks the given service as started and stores start time.
func (h *Health) StartService(serviceName string) {
if service, ok := h.Services[serviceName]; ok {
service.Status = ServiceStatusStarted

Expand All @@ -41,7 +49,8 @@ func (h *ApplicationHealth) StartService(serviceName string) {
}
}

func (h *ApplicationHealth) FailService(serviceName string, err error) {
// FailService marks the given service as failed and stores the error.
func (h *Health) FailService(serviceName string, err error) {
if service, ok := h.Services[serviceName]; ok {
service.Status = ServiceStatusError

Expand All @@ -54,18 +63,20 @@ func (h *ApplicationHealth) FailService(serviceName string, err error) {
}
}

func (h *ApplicationHealth) SetServiceData(serviceName string, data any) {
// SetServiceData stores additional health payload for the given service.
func (h *Health) SetServiceData(serviceName string, data any) {
if service, ok := h.Services[serviceName]; ok {
service.Data = data
h.Services[serviceName] = service
}
}

func (h *ApplicationHealth) String() string {
func (h *Health) String() string {
b, _ := json.Marshal(h)
return string(b)
}

func (h *ApplicationHealth) StartApplication() {
// StartApplication marks application start time.
func (h *Health) StartApplication() {
h.StartedAt = time.Now()
}
6 changes: 4 additions & 2 deletions application/healthcheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import (
)

type healther interface {
Health(context.Context) *ApplicationHealth
Health(context.Context) *Health
}

// HealthCheckHandler serves application health information as JSON.
type HealthCheckHandler struct {
app healther
}

// NewHealthCheckHandler creates a HealthCheckHandler for the given application.
func NewHealthCheckHandler(app healther) *HealthCheckHandler {
return &HealthCheckHandler{app: app}
}
Expand All @@ -28,6 +30,6 @@ func (h *HealthCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {

err := json.NewEncoder(w).Encode(health)
if err != nil {
log.ErrorContext(r.Context(), "failed to decode response to json", "error", err)
log.ErrorContext(r.Context(), "failed to encode response to json", "error", err)
}
}
100 changes: 100 additions & 0 deletions demo-app/cmd/scheduler-cron/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package main

import (
"context"
"fmt"
"time"

"github.com/platforma-dev/platforma/application"
"github.com/platforma-dev/platforma/log"
"github.com/platforma-dev/platforma/scheduler"
)

func dailyBackup(ctx context.Context) error {
log.InfoContext(ctx, "executing daily backup task")
return nil
}

func weekdayReport(ctx context.Context) error {
log.InfoContext(ctx, "generating weekday report")
return nil
}

func frequentHealthCheck(ctx context.Context) error {
log.InfoContext(ctx, "performing health check")
return nil
}

func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Example 1: Using @every syntax - every 5 seconds
s1, err := scheduler.New("@every 5s", application.RunnerFunc(func(ctx context.Context) error {
log.InfoContext(ctx, "@every syntax: every 5 seconds")
return nil
}))
if err != nil {
log.ErrorContext(ctx, "failed to create scheduler 1", "error", err)
return
}

// Example 2: Using @every syntax - every 3 seconds
s2, err := scheduler.New("@every 3s", application.RunnerFunc(func(ctx context.Context) error {
log.InfoContext(ctx, "@every syntax: every 3 seconds")
return nil
}))
if err != nil {
log.ErrorContext(ctx, "failed to create scheduler 2", "error", err)
return
}

// Example 3: Daily task (would run at midnight, but won't execute in this demo)
s3, err := scheduler.New("@daily", application.RunnerFunc(dailyBackup))
if err != nil {
log.ErrorContext(ctx, "failed to create scheduler 3", "error", err)
return
}

// Example 4: Weekday task (would run at 9 AM on weekdays, won't execute in this demo)
s4, err := scheduler.New("0 9 * * MON-FRI", application.RunnerFunc(weekdayReport))
if err != nil {
log.ErrorContext(ctx, "failed to create scheduler 4", "error", err)
return
}

// Example 5: Hourly task (won't execute in this demo)
s5, err := scheduler.New("@hourly", application.RunnerFunc(frequentHealthCheck))
if err != nil {
log.ErrorContext(ctx, "failed to create scheduler 5", "error", err)
return
}

fmt.Println("Starting cron scheduler demo...")
fmt.Println("Active schedulers:")
fmt.Println(" 1. Every 5 seconds (@every 5s)")
fmt.Println(" 2. Every 3 seconds (@every 3s)")
fmt.Println(" 3. Daily at midnight (@daily) - won't execute in demo")
fmt.Println(" 4. Weekdays at 9 AM (0 9 * * MON-FRI) - won't execute in demo")
fmt.Println(" 5. Hourly (@hourly) - won't execute in demo")
fmt.Println()
fmt.Println("Watch the logs for executions. Demo will run for 15 seconds.")
fmt.Println()

// Start all schedulers in background
go s1.Run(ctx)
go s2.Run(ctx)
go s3.Run(ctx)
go s4.Run(ctx)
go s5.Run(ctx)

// Run for 15 seconds to demonstrate the frequent tasks
time.Sleep(15 * time.Second)
cancel()

// Allow graceful shutdown
time.Sleep(100 * time.Millisecond)

fmt.Println()
fmt.Println("Demo completed!")
}
6 changes: 5 additions & 1 deletion demo-app/cmd/scheduler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ func scheduledTask(ctx context.Context) error {
func main() {
ctx, cancel := context.WithCancel(context.Background())

s := scheduler.New(time.Second, application.RunnerFunc(scheduledTask))
s, err := scheduler.New("@every 1s", application.RunnerFunc(scheduledTask))
if err != nil {
log.ErrorContext(ctx, "failed to create scheduler", "error", err)
return
}

go func() {
time.Sleep(3500 * time.Millisecond)
Expand Down
Loading