Files
orbit/cmd/orbit/main.go

271 lines
9.0 KiB
Go

package main
import (
"context"
"errors"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.yuntuo.hk/k2safe/orbit/internal/config"
"git.yuntuo.hk/k2safe/orbit/internal/observability"
"git.yuntuo.hk/k2safe/orbit/internal/storage/mysql"
"git.yuntuo.hk/k2safe/orbit/internal/transport/httpserver"
core "git.yuntuo.hk/k2safe/orbit/model"
"git.yuntuo.hk/k2safe/orbit/platform/events"
"git.yuntuo.hk/k2safe/orbit/platform/resources"
"git.yuntuo.hk/k2safe/orbit/pulse/schedule"
"git.yuntuo.hk/k2safe/orbit/pulse/service"
"git.yuntuo.hk/k2safe/orbit/pulse/worker"
"git.yuntuo.hk/k2safe/orbit/weave/control"
"git.yuntuo.hk/k2safe/orbit/weave/definition"
"git.yuntuo.hk/k2safe/orbit/weave/instance"
"git.yuntuo.hk/k2safe/orbit/weave/recovery"
weavesignal "git.yuntuo.hk/k2safe/orbit/weave/signal"
)
func main() {
if err := run(); err != nil {
slog.Error("orbit stopped", "error", err)
os.Exit(1)
}
}
func run() error {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
cfg, err := config.Load()
if err != nil {
return err
}
root, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
startup, cancel := context.WithTimeout(root, 10*time.Second)
db, err := mysql.Open(startup, cfg.MySQLDSN, cfg.MaxOpen, cfg.MaxIdle)
if err != nil {
cancel()
return err
}
defer db.Close()
if err := mysql.CheckSchema(startup, db); err != nil {
cancel()
return err
}
cancel()
var metrics *observability.Registry
var metricsListener net.Listener
if cfg.MetricsAddr != "" {
metricsListener, err = net.Listen("tcp", cfg.MetricsAddr)
if err != nil {
return errors.New("metrics listener could not bind")
}
defer metricsListener.Close()
metrics = observability.NewRegistry()
metrics.RecordDatabase(db.Stats())
}
ready := func(ctx context.Context) error {
if err := db.PingContext(ctx); err != nil {
return err
}
return mysql.CheckSchema(ctx, db)
}
server := httpserver.New(cfg.HTTPAddr, ready, service.New(mysql.NewTaskStore(db)))
httpserver.RegisterApplicationResources(server, resources.New(mysql.NewTaskStore(db)))
node, err := core.NewID()
if err != nil {
return err
}
executions, err := mysql.NewExecutionStore(db, node, mysql.ExecutionOptions{})
if err != nil {
return err
}
hub := httpserver.NewWorkerHub(root, worker.New(executions))
httpserver.RegisterWorkers(server, hub)
eventService := events.New(mysql.NewEventStore(db))
eventHub := httpserver.NewEventHub(root, eventService)
httpserver.RegisterEventSubscriptions(server, eventService)
httpserver.RegisterEventConsumers(server, eventHub)
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = eventHub.Shutdown(ctx)
}()
schedules, err := mysql.NewScheduleStore(db, node)
if err != nil {
return err
}
httpserver.RegisterSchedules(server, schedule.New(schedules))
httpserver.RegisterWorkflowDefinitions(server, definition.New(mysql.NewWorkflowDefinitionStore(db)))
httpserver.RegisterWorkflows(server, instance.New(mysql.NewWorkflowInstanceStore(db)), control.New(mysql.NewWorkflowControlStore(db)), weavesignal.New(mysql.NewWorkflowSignalStore(db)), recovery.New(mysql.NewWorkflowRecoveryStore(db)))
httpserver.WrapMetrics(server, metrics)
workflows, err := mysql.NewWorkflowRuntime(db, node)
if err != nil {
return err
}
workflowCtx, stopWorkflows := context.WithCancel(root)
workflowDone := make(chan struct{})
go func() {
defer close(workflowDone)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-workflowCtx.Done():
return
case <-ticker.C:
started := time.Now()
call, cancel := context.WithTimeout(workflowCtx, 5*time.Second)
_, err := workflows.Tick(call, 100)
cancel()
metrics.RecordBatch("workflow", started, err)
if err != nil && workflowCtx.Err() == nil {
slog.Error("workflow batch failed", "node_id", node)
}
}
}
}()
defer func() { stopWorkflows(); <-workflowDone }()
scheduleCtx, stopSchedules := context.WithCancel(root)
scheduleDone := make(chan struct{})
go func() {
defer close(scheduleDone)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-scheduleCtx.Done():
return
case <-ticker.C:
started := time.Now()
call, cancel := context.WithTimeout(scheduleCtx, 5*time.Second)
_, err := schedules.Tick(call, 100)
cancel()
metrics.RecordBatch("schedule", started, err)
if err != nil && scheduleCtx.Err() == nil {
slog.Error("schedule batch failed", "node_id", node)
}
}
}
}()
defer func() { stopSchedules(); <-scheduleDone }()
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = hub.Shutdown(ctx)
}()
recoveryDone := make(chan struct{})
recoveryCtx, stopRecovery := context.WithCancel(root)
go func() {
defer close(recoveryDone)
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for {
select {
case <-recoveryCtx.Done():
return
case <-ticker.C:
started := time.Now()
call, cancel := context.WithTimeout(recoveryCtx, 5*time.Second)
_, err := executions.Recover(call, 100)
cancel()
metrics.RecordBatch("execution_recovery", started, err)
if err != nil && recoveryCtx.Err() == nil {
slog.Error("execution recovery batch failed", "node_id", node)
}
}
}
}()
defer func() { stopRecovery(); <-recoveryDone }()
if metrics != nil {
metricsCtx, stopMetrics := context.WithCancel(root)
metricsDone := make(chan struct{})
go func() {
defer close(metricsDone)
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-metricsCtx.Done():
return
case <-ticker.C:
metrics.RecordDatabase(db.Stats())
metrics.RecordProcess()
started := time.Now()
call, cancel := context.WithTimeout(metricsCtx, 900*time.Millisecond)
snapshot, err := mysql.ReadOperationalSnapshot(call, db)
cancel()
metrics.RecordBusinessSnapshot(observability.BusinessSnapshot{
TaskStates: snapshot.TaskStates, TaskOldestAgeSeconds: snapshot.TaskOldestAgeSeconds,
DeliveryStates: snapshot.DeliveryStates, DeliveryOldestAgeSeconds: snapshot.DeliveryOldestAgeSeconds,
WorkflowStates: snapshot.WorkflowStates, WorkflowOldestAgeSeconds: snapshot.WorkflowOldestAgeSeconds,
InboxStates: snapshot.InboxStates, InboxOldestAgeSeconds: snapshot.InboxOldestAgeSeconds,
RecentEvents: snapshot.RecentEvents,
TaskTerminalLatencySeconds: snapshot.TaskTerminalLatencySeconds, TaskTerminalLatencyMaxSeconds: snapshot.TaskTerminalLatencyMaxSeconds,
WorkflowTerminalLatencySeconds: snapshot.WorkflowTerminalLatencySeconds, WorkflowTerminalLatencyMaxSeconds: snapshot.WorkflowTerminalLatencyMaxSeconds,
TaskAcceptToStartLatencySeconds: snapshot.TaskAcceptToStartLatencySeconds, TaskAcceptToStartLatencyMaxSeconds: snapshot.TaskAcceptToStartLatencyMaxSeconds,
TaskAcceptToTerminalLatencySeconds: snapshot.TaskAcceptToTerminalLatencySeconds, TaskAcceptToTerminalLatencyMaxSeconds: snapshot.TaskAcceptToTerminalLatencyMaxSeconds,
WorkflowStartToTerminalLatencySeconds: snapshot.WorkflowStartToTerminalLatencySeconds, WorkflowStartToTerminalLatencyMaxSeconds: snapshot.WorkflowStartToTerminalLatencyMaxSeconds,
Truncated: snapshot.Truncated,
}, started, err)
if err != nil && metricsCtx.Err() == nil {
reason := "query_error"
if errors.Is(err, context.DeadlineExceeded) {
reason = "timeout"
} else if errors.Is(err, context.Canceled) {
reason = "cancelled"
}
slog.Error("business metrics snapshot failed", "node_id", node, "reason", reason)
}
}
}
}()
defer func() { stopMetrics(); <-metricsDone }()
}
listener, err := net.Listen("tcp", cfg.HTTPAddr)
if err != nil {
return err
}
defer server.Close()
var metricsServer *http.Server
done := make(chan error, 2)
if metrics != nil {
metricsServer = httpserver.NewMetricsServer(cfg.MetricsAddr, metrics)
defer metricsServer.Close()
go func() { done <- metricsServer.Serve(metricsListener) }()
slog.Info("Orbit metrics server starting", "metrics_address", metricsListener.Addr().String())
}
go func() { done <- server.Serve(listener) }()
slog.Info("Orbit HTTP server starting", "address", listener.Addr().String(), "scope", "health, task, queue, schedule, subscription, workflow and Worker APIs", "node_id", node)
select {
case err := <-done:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case <-root.Done():
shutdown, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(shutdown); err != nil {
_ = server.Close()
return err
}
if metricsServer != nil {
if err := metricsServer.Shutdown(shutdown); err != nil {
return err
}
}
count := 1
if metricsServer != nil {
count++
}
for i := 0; i < count; i++ {
if err := <-done; !errors.Is(err, http.ErrServerClosed) {
return err
}
}
return nil
}
}