From f1b6c88ce9fd6fbc92ee9e3fe35448bd8447620b Mon Sep 17 00:00:00 2001 From: jm20122012 Date: Wed, 3 Jul 2024 11:52:53 -0600 Subject: [PATCH] Started adding API endpoints, worked on race condition issues --- controller/cmd/controllerservice/main.go | 14 ++- controller/internal/api/api.go | 86 ++++++++++++++++--- controller/internal/api/control.go | 1 + controller/internal/api/status.go | 1 + .../internal/controllerservice/apihandler.go | 31 +++++++ .../controllerservice/controllerservice.go | 34 ++++---- .../{api => controllerservice}/dryrun.go | 2 +- 7 files changed, 136 insertions(+), 33 deletions(-) create mode 100644 controller/internal/api/control.go create mode 100644 controller/internal/api/status.go create mode 100644 controller/internal/controllerservice/apihandler.go rename controller/internal/{api => controllerservice}/dryrun.go (95%) diff --git a/controller/cmd/controllerservice/main.go b/controller/cmd/controllerservice/main.go index dd8e37d..0a807d6 100644 --- a/controller/cmd/controllerservice/main.go +++ b/controller/cmd/controllerservice/main.go @@ -62,14 +62,20 @@ func main() { cancel() }(cancel) - var apiHndlr api.IApiHandler + wg := &sync.WaitGroup{} + + // Instantiate the API and start it + wg.Add(1) + api := api.NewApi(ctx, logger, wg) + go api.Run() + + var apiHndlr cs.IApiHandler if cfg.AppConfig.DryRun { - apiHndlr = api.NewDryRunApiHandler(ctx, logger, cfg.AppConfig.ApiUrl) + apiHndlr = cs.NewDryRunApiHandler(ctx, logger, cfg.AppConfig.ApiUrl) } else { - apiHndlr = api.NewApiHandler(ctx, logger, cfg.AppConfig.ApiUrl) + apiHndlr = cs.NewApiHandler(ctx, logger, cfg.AppConfig.ApiUrl) } - wg := &sync.WaitGroup{} wg.Add(1) controllerService := cs.NewControllerService( ctx, diff --git a/controller/internal/api/api.go b/controller/internal/api/api.go index 6b5eaff..0c6930d 100644 --- a/controller/internal/api/api.go +++ b/controller/internal/api/api.go @@ -2,30 +2,92 @@ package api import ( "context" + "encoding/json" + "fmt" "log/slog" - "sprinkler-controller-service/internal/config" + "net/http" + "sync" + "time" ) -type IApiHandler interface { - SendSprinklerEventRequest(event *config.ScheduleItem) error +type StatusResponse struct { + MicrocontrollerStatus string `json:"microcontrollerStatus"` + SprinklerStatus []SprinklerStatus `json:"sprinklerStatus"` } - -type ApiHandler struct { +type SprinklerStatus struct { + ZoneName string `json:"zoneName"` + IsActive bool `json:"isActive"` +} +type Api struct { Ctx context.Context Logger *slog.Logger - ApiUrl string + Wg *sync.WaitGroup + Mux *http.ServeMux } -func NewApiHandler(ctx context.Context, logger *slog.Logger, apiUrl string) IApiHandler { - return &ApiHandler{ +func NewApi(ctx context.Context, logger *slog.Logger, wg *sync.WaitGroup) *Api { + mux := http.NewServeMux() + mux.HandleFunc("GET /ping", pingHandler) + mux.HandleFunc("GET /system-status", getSystemStatus) + + return &Api{ Ctx: ctx, + Mux: mux, Logger: logger, - ApiUrl: apiUrl, + Wg: wg, + } +} + +func (a *Api) Run() { + server := &http.Server{ + Addr: ":8080", + Handler: a.Mux, + } + + a.Logger.Info("Starting API...") + err := server.ListenAndServe() + if err != nil { + a.Logger.Error("Error starting API", "error", err) + } + + <-a.Ctx.Done() + + a.Logger.Info("Done context signal received in API - exiting") + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + err = server.Shutdown(shutdownCtx) + if err != nil { + a.Logger.Error("Error shutting down http server", "error", err) + } + + a.Wg.Done() +} + +func pingHandler(w http.ResponseWriter, r *http.Request) { + resp := map[string]string{ + "ping": "OK", } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) } -func (a *ApiHandler) SendSprinklerEventRequest(event *config.ScheduleItem) error { - a.Logger.Info("Sending event to API", "event", event) +func getSystemStatus(w http.ResponseWriter, r *http.Request) { + + resp := StatusResponse{ + MicrocontrollerStatus: "OKAY", + } + + for i := 0; i < 5; i++ { + s := SprinklerStatus{ + ZoneName: fmt.Sprintf("zone%d", i), + IsActive: false, + } + resp.SprinklerStatus = append(resp.SprinklerStatus, s) + } - return nil + w.Header().Set("Content-Type", "application-json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) } diff --git a/controller/internal/api/control.go b/controller/internal/api/control.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/controller/internal/api/control.go @@ -0,0 +1 @@ +package api diff --git a/controller/internal/api/status.go b/controller/internal/api/status.go new file mode 100644 index 0000000..778f64e --- /dev/null +++ b/controller/internal/api/status.go @@ -0,0 +1 @@ +package api diff --git a/controller/internal/controllerservice/apihandler.go b/controller/internal/controllerservice/apihandler.go new file mode 100644 index 0000000..015b4d5 --- /dev/null +++ b/controller/internal/controllerservice/apihandler.go @@ -0,0 +1,31 @@ +package controllerservice + +import ( + "context" + "log/slog" + "sprinkler-controller-service/internal/config" +) + +type IApiHandler interface { + SendSprinklerEventRequest(event *config.ScheduleItem) error +} + +type ApiHandler struct { + Ctx context.Context + Logger *slog.Logger + ApiUrl string +} + +func NewApiHandler(ctx context.Context, logger *slog.Logger, apiUrl string) IApiHandler { + return &ApiHandler{ + Ctx: ctx, + Logger: logger, + ApiUrl: apiUrl, + } +} + +func (a *ApiHandler) SendSprinklerEventRequest(event *config.ScheduleItem) error { + a.Logger.Info("Sending event to API", "event", event) + + return nil +} diff --git a/controller/internal/controllerservice/controllerservice.go b/controller/internal/controllerservice/controllerservice.go index dc4e4d1..4e3dafb 100644 --- a/controller/internal/controllerservice/controllerservice.go +++ b/controller/internal/controllerservice/controllerservice.go @@ -3,7 +3,6 @@ package controllerservice import ( "context" "log/slog" - "sprinkler-controller-service/internal/api" "sprinkler-controller-service/internal/config" "sync" "time" @@ -19,9 +18,9 @@ type ControllerService struct { Wg *sync.WaitGroup Logger *slog.Logger Config *config.Config - ApiHandler api.IApiHandler + ApiHandler IApiHandler LastResetDate time.Time - Mutex sync.Mutex + Mutex sync.RWMutex TaskQueue chan *config.ScheduleItem } @@ -29,7 +28,7 @@ func NewControllerService(ctx context.Context, wg *sync.WaitGroup, logger *slog.Logger, cfg *config.Config, - apiHdnlr api.IApiHandler, + apiHdnlr IApiHandler, ) *ControllerService { return &ControllerService{ Ctx: ctx, @@ -38,7 +37,7 @@ func NewControllerService(ctx context.Context, Config: cfg, ApiHandler: apiHdnlr, LastResetDate: time.Now(), - Mutex: sync.Mutex{}, + Mutex: sync.RWMutex{}, TaskQueue: make(chan *config.ScheduleItem, 100), } } @@ -80,30 +79,31 @@ func (c *ControllerService) Run() { for zoneName, zoneInfo := range c.Config.ZoneList { c.Logger.Debug("Checking zone schedule", "zone", zoneName) for idx := range zoneInfo.Schedule { - scheduleItem := zoneInfo.Schedule[idx] currentTime := time.Now() - c.Logger.Debug("Comparing current and start times", "zone", zoneName, "current", currentTime, "startTime", scheduleItem.StartTime) + c.Logger.Debug("Comparing current and start times", "zone", zoneName, "current", currentTime, "startTime", zoneInfo.Schedule[idx].StartTime) - startTime, err := time.Parse(time.TimeOnly, scheduleItem.StartTime) + startTime, err := time.Parse(time.TimeOnly, zoneInfo.Schedule[idx].StartTime) if err != nil { - c.Logger.Error("Error parsing start time", "startTime", scheduleItem.StartTime, "error", err) + c.Logger.Error("Error parsing start time", "startTime", zoneInfo.Schedule[idx].StartTime, "error", err) } - duration := time.Duration(scheduleItem.DurationMinutes) + duration := time.Duration(zoneInfo.Schedule[idx].DurationMinutes) endTime := startTime.Add(duration * time.Minute) - if currentTime.After(startTime) && !scheduleItem.Active { + c.Mutex.RLock() + if currentTime.After(startTime) && !zoneInfo.Schedule[idx].Active { c.Logger.Debug("Zone is not active and current time exceeds start time for zone schedule item", "zone", zoneName, "currentTime", time.Now(), "startTime", startTime) - c.Logger.Info("Starting sprinkler event", "zoneName", zoneName, "currentTime", currentTime, "startTime", scheduleItem.StartTime, "endTime", endTime, "durationMinutes", scheduleItem.DurationMinutes) - c.TaskQueue <- &scheduleItem + c.Logger.Info("Starting sprinkler event", "zoneName", zoneName, "currentTime", currentTime, "startTime", zoneInfo.Schedule[idx].StartTime, "endTime", endTime, "durationMinutes", zoneInfo.Schedule[idx].DurationMinutes) + c.TaskQueue <- &zoneInfo.Schedule[idx] } - if currentTime.After(endTime) && scheduleItem.Active { + if currentTime.After(endTime) && zoneInfo.Schedule[idx].Active { c.Logger.Debug("Zone is active and current time exceeds end time for zone schedule item", "zone", zoneName, "currentTime", time.Now(), "startTime", startTime) - c.Logger.Info("Stopping sprinkler event", "zoneName", zoneName, "currentTime", currentTime, "startTime", scheduleItem.StartTime, "endTime", endTime, "durationMinutes", scheduleItem.DurationMinutes) - c.TaskQueue <- &scheduleItem + c.Logger.Info("Stopping sprinkler event", "zoneName", zoneName, "currentTime", currentTime, "startTime", zoneInfo.Schedule[idx].StartTime, "endTime", endTime, "durationMinutes", zoneInfo.Schedule[idx].DurationMinutes) + c.TaskQueue <- &zoneInfo.Schedule[idx] } + c.Mutex.RUnlock() } } } @@ -126,9 +126,11 @@ func (c *ControllerService) TaskProcessor(wg *sync.WaitGroup, ctx context.Contex c.Logger.Error("API request error", "event", t) continue } + c.Mutex.Lock() t.Mutex.Lock() t.Active = true t.Mutex.Unlock() + c.Mutex.Unlock() } } } diff --git a/controller/internal/api/dryrun.go b/controller/internal/controllerservice/dryrun.go similarity index 95% rename from controller/internal/api/dryrun.go rename to controller/internal/controllerservice/dryrun.go index d243c8c..b955afe 100644 --- a/controller/internal/api/dryrun.go +++ b/controller/internal/controllerservice/dryrun.go @@ -1,4 +1,4 @@ -package api +package controllerservice import ( "context"