Skip to content

Conversation

@louisinger
Copy link
Collaborator

@louisinger louisinger commented Dec 2, 2025

@altafan please review

Summary by CodeRabbit

  • New Features

    • Scheduler now supports a configurable tick interval (default 10s) and runs on a ticker-driven loop.
    • Scheduled tasks run concurrently for improved throughput.
  • Bug Fixes

    • Improved error handling/logging so the scheduler continues operating when task fetches fail.
  • Tests

    • New test suite verifying scheduler behavior, including one-shot task scheduling across implementations.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 2, 2025

Walkthrough

Adds an Option-based configurable ticker interval to the block scheduler, changes NewScheduler to accept options, refactors Start to use a time.Ticker driven by the configured interval for periodic task fetching and concurrent execution, and adds tests that exercise schedulers against a mock block-tip HTTP endpoint.

Changes

Cohort / File(s) Summary
Scheduler service implementation
internal/infrastructure/scheduler/block/service.go
Added Option type and WithTickerInterval(time.Duration) function; added tickerInterval time.Duration field to service; changed NewScheduler(esploraURL string, opts ...Option) to accept options and set a default 10s interval; refactored initialization to apply options; replaced fixed sleep loop with a time.Ticker driven by tickerInterval, fetching tasks on each tick, logging fetch errors and continuing, and executing tasks concurrently.
Scheduler tests
internal/infrastructure/scheduler/service_test.go
New test file adding TestScheduleTask which constructs and starts multiple scheduler implementations (block-based and GoCron) backed by a mock HTTP /blocks/tip/height endpoint, schedules a one-shot task with a 2s delay, waits, and asserts the scheduled handler executed; registers cleanups to stop schedulers and mock server.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Verify Option application order and that default tickerInterval is properly overridden by options.
  • Inspect time.Ticker lifecycle: creation, Stop call, and channel handling in Start to avoid goroutine/resource leaks.
  • Review concurrent task execution for correct synchronization and error handling.
  • Check tests for timing sensitivity, mock server lifecycle, and proper scheduler Start/Stop cleanup.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'unit test ports.SchedulerService' is partially related to the changeset but incomplete; while testing is added, the main change is making the scheduler ticker configurable via an Option pattern. Consider a more specific title that captures the primary change, such as 'Make scheduler ticker interval configurable' or 'Add configurable ticker interval to scheduler service'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b89026f and d0f0e24.

📒 Files selected for processing (1)
  • internal/infrastructure/scheduler/service_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-28T08:21:01.170Z
Learnt from: louisinger
Repo: arkade-os/arkd PR: 686
File: internal/core/application/fraud.go:47-61
Timestamp: 2025-08-28T08:21:01.170Z
Learning: In reactToFraud function in internal/core/application/fraud.go, the goroutine that waits for confirmation and schedules checkpoint sweep should use context.Background() instead of the request context, as this is intentional design to decouple the checkpoint sweep scheduling from the request lifetime.

Applied to files:

  • internal/infrastructure/scheduler/service_test.go
🧬 Code graph analysis (1)
internal/infrastructure/scheduler/service_test.go (2)
internal/core/ports/scheduler.go (1)
  • SchedulerService (10-18)
internal/infrastructure/scheduler/block/service.go (2)
  • NewScheduler (32-55)
  • WithTickerInterval (18-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build and Scan
  • GitHub Check: integration tests
  • GitHub Check: unit tests
🔇 Additional comments (3)
internal/infrastructure/scheduler/service_test.go (3)

1-15: LGTM!

Clean imports and external test package pattern. The use of sync/atomic indicates proper concurrency handling.


17-20: LGTM!

Simple, clean helper struct for parameterizing tests across scheduler implementations.


44-67: Well-designed test setup.

The mock server correctly simulates the block tip endpoint with atomic operations for thread safety, and the 1-second ticker interval ensures faster test execution. Clean implementation.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/infrastructure/scheduler/block/service.go (1)

81-84: Potential panic if Stop() is called multiple times or before Start().

Sending to stopCh followed by close(stopCh) will panic if Stop() is called twice or if no goroutine is reading from the channel yet. Consider using a sync.Once or a non-blocking send pattern.

Example fix using sync.Once:

 type service struct {
 	tipURL         string
 	lock           sync.Locker
 	taskes         map[int64][]func()
 	stopCh         chan struct{}
 	tickerInterval time.Duration
+	stopOnce       sync.Once
 }

 func (s *service) Stop() {
-	s.stopCh <- struct{}{}
-	close(s.stopCh)
+	s.stopOnce.Do(func() {
+		close(s.stopCh)
+	})
 }

This also simplifies Stop() by just closing the channel (the select will receive from a closed channel).

🧹 Nitpick comments (2)
internal/infrastructure/scheduler/service_test.go (1)

37-37: Consider reducing the sleep duration for faster tests.

The test sleeps for 3 seconds. Combined with the block scheduler's 1-second ticker interval, this could be optimized. Consider using a polling loop with a shorter timeout or reducing the sleep to 2 * time.Second to match the minimal expected execution window more closely while keeping tests fast.

internal/infrastructure/scheduler/block/service.go (1)

27-27: Typo: taskes should be tasks.

The field name taskes appears to be a typo and should be tasks for clarity. This also applies to the local variables and method name popTaskes.

 type service struct {
 	tipURL         string
 	lock           sync.Locker
-	taskes         map[int64][]func()
+	tasks          map[int64][]func()
 	stopCh         chan struct{}
 	tickerInterval time.Duration
 }

Similar renames would be needed for popTaskespopTasks and related local variables.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ad67c6 and 79d0bd4.

📒 Files selected for processing (2)
  • internal/infrastructure/scheduler/block/service.go (2 hunks)
  • internal/infrastructure/scheduler/service_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
internal/infrastructure/scheduler/block/service.go (1)
internal/core/ports/scheduler.go (1)
  • SchedulerService (10-18)
internal/infrastructure/scheduler/service_test.go (2)
internal/core/ports/scheduler.go (1)
  • SchedulerService (10-18)
internal/infrastructure/scheduler/block/service.go (2)
  • NewScheduler (32-55)
  • WithTickerInterval (18-22)
🔇 Additional comments (3)
internal/infrastructure/scheduler/block/service.go (3)

16-22: LGTM!

The Option pattern is correctly implemented, providing a clean way to configure the ticker interval.


32-55: LGTM!

The NewScheduler function properly initializes defaults and applies options. The default 10-second ticker interval is reasonable for block-based scheduling.


57-79: LGTM!

The Start() method now correctly uses time.Ticker for periodic execution instead of a fixed sleep. The ticker is properly stopped via defer ticker.Stop(), and the select handles both the stop signal and tick events appropriately.

Comment on lines +44 to +79
func servicesToTest(t *testing.T) []service {
// mock esplora server for block tip endpoint
var blockHeight int64 = 99
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/blocks/tip/height" {
w.WriteHeader(http.StatusOK)
height := atomic.AddInt64(&blockHeight, 1)
// nolint:errcheck
fmt.Fprintf(w, "%d", height)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(func() {
mockServer.Close()
})

blockService, err := blockscheduler.NewScheduler(
mockServer.URL,
blockscheduler.WithTickerInterval(time.Second*1),
)
if err != nil {
t.Fatalf("failed to create block scheduler: %v", err)
}

svcs := []service{
{name: "gocron", scheduler: timescheduler.NewScheduler()},
{name: "block", scheduler: blockService},
}

for _, svc := range svcs {
svc.scheduler.Start()
t.Cleanup(func() { svc.scheduler.Stop() })
}

return svcs
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cleanup closure captures loop variable—only the last scheduler will be stopped.

In the cleanup loop (lines 74-77), the closure captures svc by reference. By the time cleanup runs, svc refers to the last element in svcs, so only the last scheduler's Stop() will be called (twice), leaving the first scheduler running.

Apply this diff to fix the issue:

 	for _, svc := range svcs {
+		svc := svc // capture range variable
 		svc.scheduler.Start()
 		t.Cleanup(func() { svc.scheduler.Stop() })
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func servicesToTest(t *testing.T) []service {
// mock esplora server for block tip endpoint
var blockHeight int64 = 99
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/blocks/tip/height" {
w.WriteHeader(http.StatusOK)
height := atomic.AddInt64(&blockHeight, 1)
// nolint:errcheck
fmt.Fprintf(w, "%d", height)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(func() {
mockServer.Close()
})
blockService, err := blockscheduler.NewScheduler(
mockServer.URL,
blockscheduler.WithTickerInterval(time.Second*1),
)
if err != nil {
t.Fatalf("failed to create block scheduler: %v", err)
}
svcs := []service{
{name: "gocron", scheduler: timescheduler.NewScheduler()},
{name: "block", scheduler: blockService},
}
for _, svc := range svcs {
svc.scheduler.Start()
t.Cleanup(func() { svc.scheduler.Stop() })
}
return svcs
func servicesToTest(t *testing.T) []service {
// mock esplora server for block tip endpoint
var blockHeight int64 = 99
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/blocks/tip/height" {
w.WriteHeader(http.StatusOK)
height := atomic.AddInt64(&blockHeight, 1)
// nolint:errcheck
fmt.Fprintf(w, "%d", height)
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(func() {
mockServer.Close()
})
blockService, err := blockscheduler.NewScheduler(
mockServer.URL,
blockscheduler.WithTickerInterval(time.Second*1),
)
if err != nil {
t.Fatalf("failed to create block scheduler: %v", err)
}
svcs := []service{
{name: "gocron", scheduler: timescheduler.NewScheduler()},
{name: "block", scheduler: blockService},
}
for _, svc := range svcs {
svc := svc // capture range variable
svc.scheduler.Start()
t.Cleanup(func() { svc.scheduler.Stop() })
}
return svcs
}
🤖 Prompt for AI Agents
internal/infrastructure/scheduler/service_test.go around lines 44-79: the
t.Cleanup closure in the for loop captures the loop variable svc, so when
cleanup runs it only stops the last scheduler; fix by creating a new local
variable per iteration and capture that instead (e.g., inside the loop assign s
:= svc then call s.scheduler.Start() and t.Cleanup(func() { s.scheduler.Stop()
})), ensuring each scheduler gets its own cleanup closure.

@altafan altafan merged commit 5c7815d into arkade-os:master Dec 8, 2025
5 checks passed
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.

2 participants