added the notify module
This commit is contained in:
397
notify/README.md
Normal file
397
notify/README.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user