Skip to content

Add ServeHTTP methods to Ticker and Tickers for REST API access#77

Merged
rustyeddy merged 7 commits intomainfrom
copilot/add-servehttp-method-ticker
Jan 4, 2026
Merged

Add ServeHTTP methods to Ticker and Tickers for REST API access#77
rustyeddy merged 7 commits intomainfrom
copilot/add-servehttp-method-ticker

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Jan 3, 2026

Enables HTTP access to ticker runtime information (name, tick count, last tick time, active status) via standard http.Handler interface. Supports both individual ticker queries and retrieving all tickers as a collection.

Changes

utils/timers.go

  • Created Tickers type as map[string]*Ticker to enable collection-level HTTP access
  • Added ServeHTTP method to Ticker returning JSON ticker state for individual tickers
  • Added ServeHTTP method to Tickers returning JSON array of all tickers
  • Added TickerInfo struct for JSON serialization
  • Added sync.RWMutex for thread-safe field access (lastTick, ticks, active)
  • Tracks ticker lifecycle with active boolean (set false when ticker stops)
  • Updated GetTickers() to return Tickers type

utils/timers_test.go

  • Table-driven tests for HTTP method handling (GET 200, others 405) on both Ticker and Tickers
  • Concurrent request safety tests for both individual and collection endpoints
  • JSON format and field validation tests
  • Tests for empty list, single ticker, and multiple tickers scenarios
  • Updated existing tests for mutex-protected field access

Example Usage

// Individual ticker endpoint
ticker := utils.NewTicker("sensor-poll", 5*time.Second, processSensorData)
http.Handle("/api/ticker/sensor", ticker)

// GET /api/ticker/sensor
// {"name":"sensor-poll","last_tick":"2026-01-03T19:36:25Z","ticks":42,"active":true}

// All tickers endpoint
tickers := utils.GetTickers()
http.Handle("/api/tickers", tickers)

// GET /api/tickers
// [{"name":"sensor-poll","last_tick":"...","ticks":42,"active":true}, {"name":"heartbeat","last_tick":"...","ticks":15,"active":true}]

The implementation follows existing patterns in the codebase (see Stats.ServeHTTP and StationManager.ServeHTTP) and maintains backward compatibility while adding thread-safety.

Original prompt

Objective

Add a ServeHTTP method to the utils.Ticker struct in utils/timers.go to enable REST API access to ticker information.

Requirements

Implementation Details

  1. Add a ServeHTTP(w http.ResponseWriter, r *http.Request) method to the Ticker struct

  2. The method should:

    • Handle GET requests to retrieve ticker information
    • Return JSON response with ticker details including:
      • Name
      • Last tick time
      • Total tick count
      • Whether the ticker is currently active
    • Return appropriate HTTP status codes
    • Handle errors gracefully
  3. Example JSON response format:

{
  "name": "ticker-name",
  "last_tick": "2026-01-03T10:30:45Z",
  "ticks": 42,
  "active": true
}

Additional Considerations

  • Follow Go best practices and idiomatic patterns
  • Ensure proper error handling
  • Add appropriate HTTP headers (Content-Type: application/json)
  • Consider adding support for other HTTP methods if appropriate (OPTIONS for CORS, etc.)
  • Maintain thread-safety if needed when accessing ticker fields

Testing

  • Write unit tests for the new ServeHTTP method
  • Use table-driven test patterns where appropriate
  • Test various scenarios (valid requests, error cases, etc.)

Code Standards

  • Run make fmt before committing
  • Ensure code passes make vet
  • All tests should pass with make test

Current File

The current utils/timers.go file is located at:
https://github.com/rustyeddy/otto/blob/bd92e5ef43c2608ac2eebc9bd85de2c734441191/utils/timers.go

Context

This is part of a Go-based IoT framework that uses REST API, WebSockets, and MQTT for communication. The Ticker struct is a wrapper around time.Ticker that provides named timers with callback functions.

This pull request was created from Copilot chat.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits January 3, 2026 19:33
Co-authored-by: rustyeddy <2903425+rustyeddy@users.noreply.github.com>
Co-authored-by: rustyeddy <2903425+rustyeddy@users.noreply.github.com>
Copilot AI changed the title [WIP] Add ServeHTTP method to utils.Ticker for REST API Add ServeHTTP method to Ticker for REST API access Jan 3, 2026
Copilot AI requested a review from rustyeddy January 3, 2026 19:37
Copy link
Copy Markdown
Owner

@rustyeddy rustyeddy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot looks good, can you also add a ServeHTTP() method on the tickers variable (may need to type it e.g. type tickers map[string]*Ticker). The ServeHTTP method on tickers should allow the list of tickers to be retrieved.

Copilot AI and others added 2 commits January 3, 2026 20:16
Co-authored-by: rustyeddy <2903425+rustyeddy@users.noreply.github.com>
Co-authored-by: rustyeddy <2903425+rustyeddy@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Jan 3, 2026

@copilot looks good, can you also add a ServeHTTP() method on the tickers variable (may need to type it e.g. type tickers map[string]*Ticker). The ServeHTTP method on tickers should allow the list of...

Added in commit 54795e4. Created Tickers type as map[string]*Ticker with a ServeHTTP method that returns all tickers as a JSON array. Updated GetTickers() to return the new type. Example:

tickers := utils.GetTickers()
http.Handle("/api/tickers", tickers)
// GET /api/tickers returns: [{"name":"ticker-1",...}, {"name":"ticker-2",...}]

All tests pass with comprehensive coverage for empty list, single ticker, multiple tickers, and concurrent access scenarios.

Copilot AI changed the title Add ServeHTTP method to Ticker for REST API access Add ServeHTTP methods to Ticker and Tickers for REST API access Jan 3, 2026
Copilot AI requested a review from rustyeddy January 3, 2026 20:20
@rustyeddy rustyeddy marked this pull request as ready for review January 4, 2026 16:36
Copilot AI review requested due to automatic review settings January 4, 2026 16:36
@rustyeddy rustyeddy merged commit 4af44d2 into main Jan 4, 2026
11 checks passed
@rustyeddy rustyeddy deleted the copilot/add-servehttp-method-ticker branch January 4, 2026 16:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds HTTP REST API access to ticker runtime information by implementing ServeHTTP methods on both individual Ticker objects and the Tickers collection type. The implementation enables querying ticker state (name, tick count, last tick time, active status) via JSON endpoints.

Key changes:

  • Added thread-safe ServeHTTP methods for individual ticker and collection access
  • Introduced mutex protection for ticker fields (lastTick, ticks, active)
  • Created comprehensive table-driven and concurrency tests for the new HTTP endpoints

Reviewed changes

Copilot reviewed 6 out of 7 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
utils/timers.go Added Tickers type, TickerInfo struct, ServeHTTP methods for both Ticker and Tickers, and mutex-protected fields for thread safety
utils/timers_test.go Added comprehensive test coverage including table-driven tests for HTTP methods, JSON validation, concurrency tests, and edge cases; updated existing tests for mutex usage
otto.go Registered new /api/timers endpoint and reordered API registrations for consistency
station/station_manager.go Removed self-registration code from Start() method, moving responsibility to otto.go for centralized API registration
cmd/ottoctl/cmd_log.go Minor formatting adjustments for output alignment
messenger/messenger_nodes_test.go Code formatting changes (spaces to tabs)
_test/system_stations_test.go Refactored test logic for station expiration verification

Comment on lines +140 to +141
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
Copy link

Copilot AI Jan 4, 2026

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 uses AI. Check for mistakes.
Comment on lines +142 to +145
if err := json.NewEncoder(w).Encode(tickerList); err != nil {
slog.Error("Failed to encode tickers list", "error", err)
return
}
Copy link

Copilot AI Jan 4, 2026

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 uses AI. Check for mistakes.
Comment on lines +126 to +138
// Collect all ticker information
tickerList := make([]TickerInfo, 0, len(ts))
for _, t := range ts {
t.mu.RLock()
info := TickerInfo{
Name: t.Name,
LastTick: t.lastTick,
Ticks: t.ticks,
Active: t.active,
}
t.mu.RUnlock()
tickerList = append(tickerList, info)
}
Copy link

Copilot AI Jan 4, 2026

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 uses AI. Check for mistakes.
Comment on lines +65 to +68
wg.Wait()

// reststations, err := cli.GetStations()
// assert.NoError(t, err)
// assert.Equal(t, 10, len(reststations))
t.Logf("Checking to insure station-009 has been expired")
assert.Equal(t, 10, len(stations))
Copy link

Copilot AI Jan 4, 2026

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 stations variable is obtained before the goroutine waits and stops station-009. The assertion on line 68 checks the length of stations which 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 stale stations variable from line 48.

Copilot uses AI. Check for mistakes.
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)
Copy link

Copilot AI Jan 4, 2026

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.

Suggested change
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 uses AI. Check for mistakes.
Comment on lines +112 to +115
if err := json.NewEncoder(w).Encode(info); err != nil {
slog.Error("Failed to encode ticker info", "error", err, "ticker", t.Name)
return
}
Copy link

Copilot AI Jan 4, 2026

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 uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants