Skip to content

Commit 8526959

Browse files
author
HeerakKashyap
committed
feat(onstart): implement all reviewer feedback
- Move OnStart logic to a dedicated, testable method. - Fix all linter issues (deep-exit and cyclomatic complexity). - Add unit tests for success and error cases. - Add comprehensive documentation with a realistic example. - Update the example app to demonstrate a real-world use case.
1 parent 0cb726b commit 8526959

File tree

5 files changed

+114
-10
lines changed

5 files changed

+114
-10
lines changed

docs/advanced-guide/startup-hooks.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Startup Hooks
2+
3+
GoFr provides a way to run synchronous jobs when your application starts, before any servers begin handling requests. This is useful for tasks like seeding a database, warming up a cache, or performing other critical setup procedures.
4+
5+
## `OnStart`
6+
7+
You can register a startup hook using the `a.OnStart()` method on your `app` instance.
8+
9+
### Usage
10+
11+
The method accepts a function with the signature `func(ctx *gofr.Context) error`.
12+
13+
- The `*gofr.Context` passed to the hook is fully initialized and provides access to all dependency-injection-managed services (e.g., `ctx.Container.SQL`, `ctx.Container.Redis`).
14+
- If any `OnStart` hook returns an error, the application will log the error and refuse to start.
15+
16+
### Example: Warming up a Cache
17+
18+
Here is an example of using `OnStart` to set an initial value in a Redis cache when the application starts.
19+
20+
```go
21+
package main
22+
23+
import (
24+
"gofr.dev/pkg/gofr"
25+
)
26+
27+
func main() {
28+
a := gofr.New()
29+
30+
// Register an OnStart hook to warm up a cache.
31+
a.OnStart(func(ctx *gofr.Context) error {
32+
ctx.Container.Logger.Info("Warming up the cache...")
33+
34+
// In a real app, this might come from a database or another service.
35+
cacheKey := "initial-data"
36+
cacheValue := "This is some data cached at startup."
37+
38+
err := ctx.Redis.Set(ctx, cacheKey, cacheValue, 0).Err()
39+
if err != nil {
40+
ctx.Container.Logger.Errorf("Failed to warm up cache: %v", err)
41+
return err // Return the error to halt startup if caching fails.
42+
}
43+
44+
ctx.Container.Logger.Info("Cache warmed up successfully!")
45+
46+
return nil
47+
})
48+
49+
// ... register your routes
50+
51+
a.Run()
52+
}
53+
```
54+
55+
This ensures that critical startup tasks are completed successfully before the application begins accepting traffic.

examples/http-server/main.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,23 @@ func main() {
2828
a.GET("/trace", TraceHandler)
2929
a.GET("/mysql", MysqlHandler)
3030

31+
// Register an OnStart hook to warm up a cache.
3132
a.OnStart(func(ctx *gofr.Context) error {
33+
ctx.Container.Logger.Info("Warming up the cache...")
34+
35+
// Example: Fetch some data and store it in Redis.
36+
// In a real app, this might come from a database or another service.
37+
cacheKey := "initial-data"
38+
cacheValue := "This is some data cached at startup."
39+
40+
err := ctx.Redis.Set(ctx, cacheKey, cacheValue, 0).Err()
41+
if err != nil {
42+
ctx.Container.Logger.Errorf("Failed to warm up cache: %v", err)
43+
return err // Return the error to halt startup if caching fails.
44+
}
45+
46+
ctx.Container.Logger.Info("Cache warmed up successfully!")
3247

33-
fmt.Println("OnStart hook executed!")
34-
fmt.Printf("SQL: %#v\n", ctx.Container.SQL)
35-
fmt.Printf("Redis: %#v\n", ctx.Container.Redis)
3648
return nil
3749
})
3850

pkg/gofr/gofr.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,17 @@ func (a *App) newContextForHooks(ctx context.Context) *Context {
5959
}
6060
}
6161

62-
func (a *App) runOnStartHooks(ctx context.Context) {
62+
func (a *App) runOnStartHooks(ctx context.Context) error {
6363
gofrCtx := a.newContextForHooks(ctx)
6464
for _, hook := range a.onStartHooks {
6565
if err := hook(gofrCtx); err != nil {
66+
// Log the error and return it
6667
a.Logger().Errorf("OnStart hook failed: %v", err)
67-
os.Exit(1)
68+
return err
6869
}
6970
}
71+
// Return nil if all hooks succeed
72+
return nil
7073
}
7174

7275
// Shutdown stops the service(s) and close the application.

pkg/gofr/gofr_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package gofr
22

33
import (
4+
"context"
45
"encoding/base64"
56
"encoding/json"
7+
"errors"
68
"fmt"
79
"io"
810
"net/http"
@@ -1162,3 +1164,35 @@ func TestApp_Subscribe(t *testing.T) {
11621164
assert.False(t, ok)
11631165
})
11641166
}
1167+
1168+
func TestApp_OnStart(t *testing.T) {
1169+
// Test case 1: Hook executes successfully
1170+
t.Run("success", func(t *testing.T) {
1171+
var hookCalled bool
1172+
app := New()
1173+
1174+
app.OnStart(func(ctx *Context) error {
1175+
hookCalled = true
1176+
return nil
1177+
})
1178+
1179+
err := app.runOnStartHooks(context.Background())
1180+
1181+
assert.Nil(t, err, "Expected no error from runOnStartHooks")
1182+
assert.True(t, hookCalled, "Expected the OnStart hook to be called")
1183+
})
1184+
1185+
// Test case 2: Hook returns an error
1186+
t.Run("error", func(t *testing.T) {
1187+
expectedErr := errors.New("hook failed")
1188+
app := New()
1189+
1190+
app.OnStart(func(ctx *Context) error {
1191+
return expectedErr
1192+
})
1193+
1194+
err := app.runOnStartHooks(context.Background())
1195+
1196+
assert.Equal(t, expectedErr, err, "Expected an error from runOnStartHooks")
1197+
})
1198+
}

pkg/gofr/run.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"os/signal"
88
"sync"
99
"syscall"
10-
1110
)
1211

1312
// Run starts the application. If it is an HTTP server, it will start the server.
@@ -16,13 +15,14 @@ func (a *App) Run() {
1615
a.cmd.Run(a.container)
1716
}
1817

19-
2018
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
2119
defer stop()
2220

23-
onStartCtx := context.WithoutCancel(ctx)
24-
a.runOnStartHooks(onStartCtx)
25-
21+
// Running startup hooks and exit if they fail.
22+
// Using the main app context to ensure proper lifecycle management.
23+
if err := a.runOnStartHooks(ctx); err != nil {
24+
os.Exit(1)
25+
}
2626

2727
timeout, err := getShutdownTimeoutFromConfig(a.Config)
2828
if err != nil {

0 commit comments

Comments
 (0)