-
Notifications
You must be signed in to change notification settings - Fork 0
Add ServeHTTP methods to Ticker and Tickers for REST API access #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4471b7d
e30f1f0
535b367
68ef7c1
54795e4
94dadfe
aae33f9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,25 +1,36 @@ | ||||||||||||||||||||||||||||||
| package utils | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import "time" | ||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||
| "log/slog" | ||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Ticker is a wrapper around time.Ticker it is given a name, it hold | ||||||||||||||||||||||||||||||
| // Ticker is a wrapper around time.Ticker it is given a name, it holds | ||||||||||||||||||||||||||||||
| // the duration and kept in a map indexed by name such that it is easy | ||||||||||||||||||||||||||||||
| // to lookup to shutdown or reset | ||||||||||||||||||||||||||||||
| type Ticker struct { | ||||||||||||||||||||||||||||||
| Name string | ||||||||||||||||||||||||||||||
| *time.Ticker | ||||||||||||||||||||||||||||||
| Func func(t time.Time) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| mu sync.RWMutex | ||||||||||||||||||||||||||||||
| lastTick time.Time | ||||||||||||||||||||||||||||||
| ticks int | ||||||||||||||||||||||||||||||
| active bool | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Tickers is a map of all active tickers indexed by name | ||||||||||||||||||||||||||||||
| type Tickers map[string]*Ticker | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||
| // Start time is the time otto started | ||||||||||||||||||||||||||||||
| StartTime time.Time | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // the map with all our tickers | ||||||||||||||||||||||||||||||
| tickers = make(map[string]*Ticker) | ||||||||||||||||||||||||||||||
| tickers = make(Tickers) | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||||||||||
|
|
@@ -34,28 +45,34 @@ func Timestamp() time.Duration { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // NewTicker creates a time.Ticker with the name n that will fire | ||||||||||||||||||||||||||||||
| // every d time.Duration. The function f will be called every time | ||||||||||||||||||||||||||||||
| // ticker goes off. The ticker can be stoped, restarted and reset | ||||||||||||||||||||||||||||||
| // ticker goes off. The ticker can be stopped, restarted and reset | ||||||||||||||||||||||||||||||
| // with a different duration | ||||||||||||||||||||||||||||||
| func NewTicker(n string, d time.Duration, f func(t time.Time)) *Ticker { | ||||||||||||||||||||||||||||||
| t := &Ticker{ | ||||||||||||||||||||||||||||||
| Name: n, | ||||||||||||||||||||||||||||||
| Ticker: time.NewTicker(d), | ||||||||||||||||||||||||||||||
| Func: f, | ||||||||||||||||||||||||||||||
| active: true, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| tickers[n] = t | ||||||||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||||||||
| for tick := range t.Ticker.C { | ||||||||||||||||||||||||||||||
| t.mu.Lock() | ||||||||||||||||||||||||||||||
| t.lastTick = time.Now() | ||||||||||||||||||||||||||||||
| t.ticks++ | ||||||||||||||||||||||||||||||
| t.mu.Unlock() | ||||||||||||||||||||||||||||||
| f(tick) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| t.mu.Lock() | ||||||||||||||||||||||||||||||
| t.active = false | ||||||||||||||||||||||||||||||
| t.mu.Unlock() | ||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||
| return t | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // GetTickers will return the map of all ticker values. | ||||||||||||||||||||||||||||||
| func GetTickers() map[string]*Ticker { | ||||||||||||||||||||||||||||||
| func GetTickers() Tickers { | ||||||||||||||||||||||||||||||
| return tickers | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
@@ -64,3 +81,66 @@ func GetTicker(n string) *Ticker { | |||||||||||||||||||||||||||||
| t, _ := tickers[n] | ||||||||||||||||||||||||||||||
| return t | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // TickerInfo holds the JSON-serializable ticker information | ||||||||||||||||||||||||||||||
| type TickerInfo struct { | ||||||||||||||||||||||||||||||
| Name string `json:"name"` | ||||||||||||||||||||||||||||||
| LastTick time.Time `json:"last_tick"` | ||||||||||||||||||||||||||||||
| Ticks int `json:"ticks"` | ||||||||||||||||||||||||||||||
| Active bool `json:"active"` | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // ServeHTTP implements http.Handler to return ticker information as JSON. | ||||||||||||||||||||||||||||||
| // It returns the ticker's name, last tick time, total tick count, and active status. | ||||||||||||||||||||||||||||||
| func (t *Ticker) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||||||
| if r.Method != http.MethodGet { | ||||||||||||||||||||||||||||||
| http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| t.mu.RLock() | ||||||||||||||||||||||||||||||
| info := TickerInfo{ | ||||||||||||||||||||||||||||||
| Name: t.Name, | ||||||||||||||||||||||||||||||
| LastTick: t.lastTick, | ||||||||||||||||||||||||||||||
| Ticks: t.ticks, | ||||||||||||||||||||||||||||||
| Active: t.active, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| t.mu.RUnlock() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| w.Header().Set("Content-Type", "application/json") | ||||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusOK) | ||||||||||||||||||||||||||||||
| if err := json.NewEncoder(w).Encode(info); err != nil { | ||||||||||||||||||||||||||||||
| slog.Error("Failed to encode ticker info", "error", err, "ticker", t.Name) | ||||||||||||||||||||||||||||||
|
Comment on lines
+111
to
+113
|
||||||||||||||||||||||||||||||
| w.WriteHeader(http.StatusOK) | |
| if err := json.NewEncoder(w).Encode(info); err != nil { | |
| slog.Error("Failed to encode ticker info", "error", err, "ticker", t.Name) | |
| data, err := json.Marshal(info) | |
| if err != nil { | |
| slog.Error("Failed to marshal ticker info", "error", err, "ticker", t.Name) | |
| http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) | |
| return | |
| } | |
| w.WriteHeader(http.StatusOK) | |
| if _, err := w.Write(data); err != nil { | |
| slog.Error("Failed to write ticker info response", "error", err, "ticker", t.Name) |
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling after json.NewEncoder().Encode() fails is incomplete. The error is logged but no HTTP error response is sent to the client. The client will receive a 200 OK status with an incomplete or malformed JSON body. Consider using http.Error() to send a proper error response with status 500 after logging the error.
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Tickers.ServeHTTP method iterates over the map without any synchronization of the map itself. While each individual Ticker's mutex protects its fields, the map iteration is not protected. If a new ticker is added or removed while this map is being iterated, it can cause a race condition or panic. The map should be protected with a mutex during iteration, or a copy/snapshot of the map should be made under lock before iteration.
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The header and status code should be set after successful encoding, not before. If json.NewEncoder().Encode() fails, the response will have a 200 OK status even though an error occurred and no valid JSON was written. Consider setting the header but deferring the WriteHeader call until after successful encoding, or handle the error by writing an error response.
Copilot
AI
Jan 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling after json.NewEncoder().Encode() fails is incomplete. The error is logged but no HTTP error response is sent to the client. The client will receive a 200 OK status with an incomplete or malformed JSON body. Consider using http.Error() to send a proper error response with status 500 after logging the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test logic is incorrect. The
stationsvariable is obtained before the goroutine waits and stops station-009. The assertion on line 68 checks the length ofstationswhich was captured earlier and won't reflect the state after the station is stopped. The assertion should check the length of a freshly fetched stations list after the goroutine completes, not the stalestationsvariable from line 48.