first commit
This commit is contained in:
156
README.md
Normal file
156
README.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# PubSub
|
||||
|
||||
A lightweight, concurrent **Publish/Subscribe** system for Go.
|
||||
It provides topic-based message distribution with worker goroutines, safe subscriber management, optional blocking or dropping of messages when queues are full, and supports programming against an interface for flexibility.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Topic-based subscriptions (`topic -> subscriber ID -> callback`)
|
||||
* Multiple workers for concurrent delivery
|
||||
* Safe subscription and unsubscription (per topic or all topics)
|
||||
* Panic-safe callback execution (one bad subscriber won’t crash workers)
|
||||
* Configurable message queue size
|
||||
* Blocking or non-blocking publish behavior
|
||||
* Graceful shutdown with `Close()`
|
||||
* Supports the `PubSub` interface for flexible usage and testing
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
go get gitlab.com/your-repo/pubSub
|
||||
```
|
||||
|
||||
Then import in your Go project:
|
||||
|
||||
```go
|
||||
import "gitlab.com/your-repo/pubSub"
|
||||
```
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gitlab.com/your-repo/pubSub"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ps := pubSub.NewPubsub(2, 100) // 2 workers, queue size 100
|
||||
|
||||
ps.Subscribe("sub1", "greetings", func(data any) {
|
||||
fmt.Println("Subscriber 1 received:", data)
|
||||
})
|
||||
|
||||
ps.Subscribe("sub2", "greetings", func(data any) {
|
||||
fmt.Println("Subscriber 2 received:", data)
|
||||
})
|
||||
|
||||
ps.Publish("greetings", "Hello, world!")
|
||||
ps.Publish("greetings", "PubSub in Go is working 🚀")
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
ps.Unsubscribe("sub1", "greetings")
|
||||
ps.Publish("greetings", "Goodbye from PubSub!")
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
ps.Close()
|
||||
}
|
||||
```
|
||||
|
||||
## 📖 API Reference
|
||||
|
||||
### `NewPubsub(workerCount, queueSize int) *Pubsub`
|
||||
|
||||
Creates a new Pubsub instance with:
|
||||
|
||||
* `workerCount`: number of goroutines delivering messages
|
||||
* `queueSize`: maximum buffered messages
|
||||
|
||||
### `Subscribe(id, topic string, cb func(any))`
|
||||
|
||||
Registers a callback under a subscriber ID for a topic.
|
||||
|
||||
* `id` must be unique per topic.
|
||||
* Callbacks are executed asynchronously by workers.
|
||||
|
||||
### `Unsubscribe(id, topic string)`
|
||||
|
||||
Removes a subscriber ID from a specific topic.
|
||||
|
||||
### `UnsubscribeAll(id string)`
|
||||
|
||||
Removes a subscriber ID from **all topics** it is subscribed to.
|
||||
|
||||
### `Publish(topic string, data any)`
|
||||
|
||||
Publishes a message to a topic.
|
||||
|
||||
* If `Blocking = true`, `Publish` will block if the queue is full.
|
||||
* If `Blocking = false`, the message will be dropped and logged.
|
||||
|
||||
### `Close()`
|
||||
|
||||
Gracefully shuts down the Pubsub:
|
||||
|
||||
* Stops accepting new messages
|
||||
* Closes the queue
|
||||
* Clears subscriptions
|
||||
* Waits for workers to finish
|
||||
|
||||
## 🧩 PubSub Interface
|
||||
|
||||
The package also defines a `PubSub` interface for flexibility and testing:
|
||||
|
||||
```go
|
||||
package pubSub
|
||||
|
||||
type PubSub interface {
|
||||
Subscribe(id, topic string, cb func(any))
|
||||
Unsubscribe(id, topic string)
|
||||
UnsubscribeAll(id string)
|
||||
Publish(topic string, data any)
|
||||
Close()
|
||||
}
|
||||
```
|
||||
|
||||
Your `Pubsub` struct implements this interface automatically.
|
||||
|
||||
### Example Using the Interface
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"gitlab.com/your-repo/pubSub"
|
||||
)
|
||||
|
||||
func runApp(ps pubSub.PubSub) {
|
||||
ps.Subscribe("sub1", "topic1", func(data any) {
|
||||
fmt.Println("Received:", data)
|
||||
})
|
||||
}
|
||||
|
||||
func main() {
|
||||
ps := pubSub.NewPubsub(2, 100) // returns a Pubsub implementing PubSub interface
|
||||
|
||||
runApp(ps)
|
||||
ps.Publish("topic1", "Hello from interface!")
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
ps.Close()
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ Notes
|
||||
|
||||
* Subscriber callbacks must be non-blocking. Long-running tasks should be offloaded to separate goroutines.
|
||||
* IDs are required so you can **unsubscribe cleanly** (important for WebSocket connections, etc.).
|
||||
* This is **not MQTT**, but a simpler, in-memory Pub/Sub suitable for local apps or servers.
|
10
interface.go
Normal file
10
interface.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package pubSub
|
||||
|
||||
// PubSub defines the public API for a publish/subscribe system.
|
||||
type PubSub interface {
|
||||
Subscribe(id, topic string, cb func(any))
|
||||
Unsubscribe(id, topic string)
|
||||
UnsubscribeAll(id string)
|
||||
Publish(topic string, data any)
|
||||
Close()
|
||||
}
|
7
models/data.go
Normal file
7
models/data.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package models
|
||||
|
||||
type Data struct {
|
||||
Action string `json:"action"`
|
||||
Topic string `json:"topic"`
|
||||
Data any `json:"data"`
|
||||
}
|
133
pubSub.go
Normal file
133
pubSub.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package pubSub
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"gitea.tecamino.com/paadi/pubSub/models"
|
||||
)
|
||||
|
||||
// Pubsub implements a simple topic-based publish/subscribe system
|
||||
// with worker goroutines delivering messages to subscriber callbacks.
|
||||
type Pubsub struct {
|
||||
mu sync.RWMutex // protects access to subs and closed
|
||||
subs map[string]map[string]func(any) // topic -> subscriberID -> callback
|
||||
closed bool // signals shutdown
|
||||
jobQueue chan models.Data // message delivery queue
|
||||
wg sync.WaitGroup // waits for workers to finish
|
||||
queueSize int // maximum buffered messages
|
||||
Blocking bool // if true, Publish blocks on full queue; if false, drops messages
|
||||
}
|
||||
|
||||
// Ensure Pubsub implements PubSub interface
|
||||
var _ PubSub = (*Pubsub)(nil)
|
||||
|
||||
// NewPubsub creates a new Pubsub with a fixed number of workers and queue size.
|
||||
func NewPubsub(workerCount, queueSize int) *Pubsub {
|
||||
ps := &Pubsub{
|
||||
subs: make(map[string]map[string]func(any)),
|
||||
jobQueue: make(chan models.Data, max(1, queueSize)),
|
||||
queueSize: max(1, queueSize),
|
||||
}
|
||||
|
||||
// start worker pool for message delivery
|
||||
for i := 0; i < max(1, workerCount); i++ {
|
||||
ps.wg.Add(1)
|
||||
go ps.worker()
|
||||
}
|
||||
return ps
|
||||
}
|
||||
|
||||
// worker pulls jobs from the queue and invokes subscriber callbacks safely.
|
||||
func (ps *Pubsub) worker() {
|
||||
defer ps.wg.Done()
|
||||
for job := range ps.jobQueue {
|
||||
ps.mu.RLock()
|
||||
subs := make([]func(any), 0, len(ps.subs[job.Topic]))
|
||||
for _, cb := range ps.subs[job.Topic] {
|
||||
subs = append(subs, cb)
|
||||
}
|
||||
ps.mu.RUnlock()
|
||||
|
||||
for _, cb := range subs {
|
||||
func(c func(any), d any) {
|
||||
defer func() { recover() }()
|
||||
c(d)
|
||||
}(cb, job.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe registers a callback for a given topic under a subscriber ID.
|
||||
func (ps *Pubsub) Subscribe(id, topic string, cb func(any)) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
if _, ok := ps.subs[topic]; !ok {
|
||||
ps.subs[topic] = make(map[string]func(any))
|
||||
}
|
||||
ps.subs[topic][id] = cb
|
||||
}
|
||||
|
||||
// Unsubscribe removes a single subscriber ID from a given topic.
|
||||
func (ps *Pubsub) Unsubscribe(id, topic string) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
if _, ok := ps.subs[topic]; ok {
|
||||
delete(ps.subs[topic], id)
|
||||
if len(ps.subs[topic]) == 0 {
|
||||
delete(ps.subs, topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UnsubscribeAll removes a subscriber ID from all topics it is registered to.
|
||||
func (ps *Pubsub) UnsubscribeAll(id string) {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
for topic := range ps.subs {
|
||||
delete(ps.subs[topic], id)
|
||||
if len(ps.subs[topic]) == 0 {
|
||||
delete(ps.subs, topic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Publish enqueues a message for a topic.
|
||||
func (ps *Pubsub) Publish(topic string, data any) {
|
||||
ps.mu.RLock()
|
||||
if ps.closed {
|
||||
ps.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
ps.mu.RUnlock()
|
||||
|
||||
if ps.Blocking {
|
||||
ps.jobQueue <- models.Data{Topic: topic, Data: data}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case ps.jobQueue <- models.Data{Topic: topic, Data: data}:
|
||||
default:
|
||||
log.Println("queue full with:", ps.queueSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Close shuts down the Pubsub: stops accepting new messages, closes the queue,
|
||||
// clears subscriptions, and waits for all workers to finish.
|
||||
func (ps *Pubsub) Close() {
|
||||
ps.mu.Lock()
|
||||
if ps.closed {
|
||||
ps.mu.Unlock()
|
||||
return
|
||||
}
|
||||
ps.closed = true
|
||||
close(ps.jobQueue)
|
||||
ps.subs = nil
|
||||
ps.mu.Unlock()
|
||||
|
||||
ps.wg.Wait()
|
||||
}
|
Reference in New Issue
Block a user