398 lines
8.2 KiB
Markdown
398 lines
8.2 KiB
Markdown
# notify
|
||
|
||
Thread-safe pub/sub notification system for Go applications.
|
||
|
||
## Features
|
||
|
||
- **Thread-Safe**: All operations are safe for concurrent use
|
||
- **Configurable Buffering**: Set custom buffer sizes per notifier
|
||
- **Targeted & Broadcast**: Send to specific subscribers or broadcast to all
|
||
- **Graceful Shutdown**: Built-in Close() for clean resource cleanup
|
||
- **Idempotent Operations**: Safe to call Unsubscribe() and Close() multiple times
|
||
- **Zero Dependencies**: Uses only Go standard library
|
||
- **Comprehensive Tests**: 95%+ code coverage with race detector clean
|
||
|
||
## Installation
|
||
|
||
```bash
|
||
go get git.haelnorr.com/h/golib/notify
|
||
```
|
||
|
||
## Quick Start
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"git.haelnorr.com/h/golib/notify"
|
||
)
|
||
|
||
func main() {
|
||
// Create a notifier with 50-notification buffer per subscriber
|
||
n := notify.NewNotifier(50)
|
||
defer n.Close()
|
||
|
||
// Subscribe to receive notifications
|
||
sub, err := n.Subscribe()
|
||
if err != nil {
|
||
panic(err)
|
||
}
|
||
defer sub.Unsubscribe()
|
||
|
||
// Listen for notifications
|
||
go func() {
|
||
for notification := range sub.Listen() {
|
||
fmt.Printf("[%s] %s: %s\n",
|
||
notification.Level,
|
||
notification.Title,
|
||
notification.Message)
|
||
}
|
||
fmt.Println("Listener exited")
|
||
}()
|
||
|
||
// Send a notification
|
||
n.Notify(notify.Notification{
|
||
Target: sub.ID,
|
||
Level: notify.LevelSuccess,
|
||
Title: "Welcome",
|
||
Message: "You're now subscribed!",
|
||
})
|
||
|
||
// Broadcast to all subscribers
|
||
n.NotifyAll(notify.Notification{
|
||
Level: notify.LevelInfo,
|
||
Title: "System Status",
|
||
Message: "All systems operational",
|
||
})
|
||
}
|
||
```
|
||
|
||
## Usage
|
||
|
||
### Creating a Notifier
|
||
|
||
The buffer size determines how many notifications can be queued per subscriber:
|
||
|
||
```go
|
||
// Unbuffered - sends block until received
|
||
n := notify.NewNotifier(0)
|
||
|
||
// Small buffer - low memory, may drop if slow readers
|
||
n := notify.NewNotifier(25)
|
||
|
||
// Recommended - balanced approach
|
||
n := notify.NewNotifier(50)
|
||
|
||
// Large buffer - handles bursts well
|
||
n := notify.NewNotifier(500)
|
||
```
|
||
|
||
### Subscribing
|
||
|
||
Each subscriber receives a unique ID and a buffered notification channel:
|
||
|
||
```go
|
||
sub, err := n.Subscribe()
|
||
if err != nil {
|
||
// Handle error (e.g., notifier is closed)
|
||
log.Fatal(err)
|
||
}
|
||
|
||
fmt.Println("Subscriber ID:", sub.ID)
|
||
```
|
||
|
||
### Listening for Notifications
|
||
|
||
Use a for-range loop to process notifications:
|
||
|
||
```go
|
||
for notification := range sub.Listen() {
|
||
switch notification.Level {
|
||
case notify.LevelSuccess:
|
||
fmt.Println("✓", notification.Message)
|
||
case notify.LevelError:
|
||
fmt.Println("✗", notification.Message)
|
||
default:
|
||
fmt.Println("ℹ", notification.Message)
|
||
}
|
||
}
|
||
```
|
||
|
||
### Sending Targeted Notifications
|
||
|
||
Send to a specific subscriber:
|
||
|
||
```go
|
||
n.Notify(notify.Notification{
|
||
Target: sub.ID,
|
||
Level: notify.LevelWarn,
|
||
Title: "Account Warning",
|
||
Message: "Password expires in 3 days",
|
||
Details: "Please update your password",
|
||
})
|
||
```
|
||
|
||
### Broadcasting to All Subscribers
|
||
|
||
Send to all current subscribers:
|
||
|
||
```go
|
||
n.NotifyAll(notify.Notification{
|
||
Level: notify.LevelInfo,
|
||
Title: "Maintenance",
|
||
Message: "System will restart in 5 minutes",
|
||
})
|
||
```
|
||
|
||
### Unsubscribing
|
||
|
||
Clean up when done (safe to call multiple times):
|
||
|
||
```go
|
||
sub.Unsubscribe()
|
||
```
|
||
|
||
### Graceful Shutdown
|
||
|
||
Close the notifier to unsubscribe all and prevent new subscriptions:
|
||
|
||
```go
|
||
n.Close()
|
||
// After Close():
|
||
// - All subscribers are removed
|
||
// - All notification channels are closed
|
||
// - Future Subscribe() calls return error
|
||
// - Notify/NotifyAll are no-ops
|
||
```
|
||
|
||
## Notification Levels
|
||
|
||
Four predefined levels are available:
|
||
|
||
| Level | Constant | Use Case |
|
||
|-------|----------|----------|
|
||
| Success | `notify.LevelSuccess` | Successful operations |
|
||
| Info | `notify.LevelInfo` | General information |
|
||
| Warning | `notify.LevelWarn` | Non-critical warnings |
|
||
| Error | `notify.LevelError` | Errors requiring attention |
|
||
|
||
## Advanced Usage
|
||
|
||
### Custom Action Data
|
||
|
||
The `Action` field can hold any data type:
|
||
|
||
```go
|
||
type UserAction struct {
|
||
URL string
|
||
Method string
|
||
}
|
||
|
||
n.Notify(notify.Notification{
|
||
Target: sub.ID,
|
||
Level: notify.LevelInfo,
|
||
Message: "New update available",
|
||
Action: UserAction{
|
||
URL: "/updates/download",
|
||
Method: "GET",
|
||
},
|
||
})
|
||
|
||
// In listener:
|
||
for notif := range sub.Listen() {
|
||
if action, ok := notif.Action.(UserAction); ok {
|
||
fmt.Printf("Action: %s %s\n", action.Method, action.URL)
|
||
}
|
||
}
|
||
```
|
||
|
||
### Multiple Subscribers
|
||
|
||
Create a notification hub for multiple clients:
|
||
|
||
```go
|
||
n := notify.NewNotifier(100)
|
||
defer n.Close()
|
||
|
||
// Create 10 subscribers
|
||
subscribers := make([]*notify.Subscriber, 10)
|
||
for i := 0; i < 10; i++ {
|
||
sub, _ := n.Subscribe()
|
||
subscribers[i] = sub
|
||
|
||
// Start listener for each
|
||
go func(id int, s *notify.Subscriber) {
|
||
for notif := range s.Listen() {
|
||
log.Printf("Sub %d: %s", id, notif.Message)
|
||
}
|
||
}(i, sub)
|
||
}
|
||
|
||
// Broadcast to all
|
||
n.NotifyAll(notify.Notification{
|
||
Level: notify.LevelSuccess,
|
||
Message: "All subscribers active",
|
||
})
|
||
```
|
||
|
||
### Concurrent-Safe Operations
|
||
|
||
All operations are thread-safe:
|
||
|
||
```go
|
||
n := notify.NewNotifier(50)
|
||
|
||
// Safe to subscribe from multiple goroutines
|
||
for i := 0; i < 100; i++ {
|
||
go func() {
|
||
sub, _ := n.Subscribe()
|
||
defer sub.Unsubscribe()
|
||
// ...
|
||
}()
|
||
}
|
||
|
||
// Safe to notify from multiple goroutines
|
||
for i := 0; i < 100; i++ {
|
||
go func() {
|
||
n.NotifyAll(notify.Notification{
|
||
Level: notify.LevelInfo,
|
||
Message: "Concurrent notification",
|
||
})
|
||
}()
|
||
}
|
||
```
|
||
|
||
## Best Practices
|
||
|
||
### 1. Use defer for Cleanup
|
||
|
||
```go
|
||
n := notify.NewNotifier(50)
|
||
defer n.Close()
|
||
|
||
sub, _ := n.Subscribe()
|
||
defer sub.Unsubscribe()
|
||
```
|
||
|
||
### 2. Check Errors
|
||
|
||
```go
|
||
sub, err := n.Subscribe()
|
||
if err != nil {
|
||
log.Printf("Subscribe failed: %v", err)
|
||
return
|
||
}
|
||
```
|
||
|
||
### 3. Buffer Size Recommendations
|
||
|
||
| Scenario | Buffer Size |
|
||
|----------|------------|
|
||
| Real-time chat | 10-25 |
|
||
| General app notifications | 50-100 |
|
||
| High-throughput logging | 200-500 |
|
||
| Testing/debugging | 0 (unbuffered) |
|
||
|
||
### 4. Listener Goroutines
|
||
|
||
Always use goroutines for listeners to prevent blocking:
|
||
|
||
```go
|
||
// Good ✓
|
||
go func() {
|
||
for notif := range sub.Listen() {
|
||
process(notif)
|
||
}
|
||
}()
|
||
|
||
// Bad ✗ - blocks main goroutine
|
||
for notif := range sub.Listen() {
|
||
process(notif)
|
||
}
|
||
```
|
||
|
||
### 5. Detect Channel Closure
|
||
|
||
```go
|
||
for notification := range sub.Listen() {
|
||
// Process notifications
|
||
}
|
||
// When this loop exits, the channel is closed
|
||
// Either subscriber unsubscribed or notifier closed
|
||
fmt.Println("No more notifications")
|
||
```
|
||
|
||
## Performance
|
||
|
||
- **Subscribe**: O(1) average case (random ID generation)
|
||
- **Notify**: O(1) lookup + O(1) channel send (non-blocking)
|
||
- **NotifyAll**: O(n) where n is number of subscribers
|
||
- **Unsubscribe**: O(1) map deletion + O(1) channel close
|
||
- **Close**: O(n) where n is number of subscribers
|
||
|
||
### Benchmarks
|
||
|
||
Typical performance on modern hardware:
|
||
|
||
- Subscribe: ~5-10µs per operation
|
||
- Notify: ~1-2µs per operation
|
||
- NotifyAll (10 subs): ~10-20µs
|
||
- Buffer full handling: ~100ns (TryLock drop)
|
||
|
||
## Thread Safety
|
||
|
||
All public methods are thread-safe:
|
||
|
||
- ✅ `NewNotifier()` - Safe
|
||
- ✅ `Subscribe()` - Safe, concurrent calls allowed
|
||
- ✅ `Unsubscribe()` - Safe, idempotent
|
||
- ✅ `Notify()` - Safe, concurrent calls allowed
|
||
- ✅ `NotifyAll()` - Safe, concurrent calls allowed
|
||
- ✅ `Close()` - Safe, idempotent
|
||
- ✅ `Listen()` - Safe, returns read-only channel
|
||
|
||
## Testing
|
||
|
||
Run tests:
|
||
|
||
```bash
|
||
# Run all tests
|
||
go test
|
||
|
||
# With race detector
|
||
go test -race
|
||
|
||
# With coverage
|
||
go test -cover
|
||
|
||
# Verbose output
|
||
go test -v
|
||
```
|
||
|
||
Current test coverage: **95.1%**
|
||
|
||
## Documentation
|
||
|
||
Full API documentation available at:
|
||
- [pkg.go.dev](https://pkg.go.dev/git.haelnorr.com/h/golib/notify)
|
||
- Or run: `go doc -all git.haelnorr.com/h/golib/notify`
|
||
|
||
## License
|
||
|
||
MIT License - see repository root for details
|
||
|
||
## Contributing
|
||
|
||
See CONTRIBUTING.md in the repository root
|
||
|
||
## Related Projects
|
||
|
||
Other modules in the golib collection:
|
||
- `cookies` - HTTP cookie utilities
|
||
- `env` - Environment variable helpers
|
||
- `ezconf` - Configuration loader
|
||
- `hlog` - Logging with zerolog
|
||
- `hws` - HTTP web server
|
||
- `jwt` - JWT token utilities
|