mirror of
https://github.com/oarkflow/mq.git
synced 2025-09-26 20:11:16 +08:00
55db1dc4b5e59a61adc18a54decc482a1553607c
Introduction MQ (Message Queue Broker)
A simple Pub/Sub system memory based task processing. It uses centralized server to manage consumers and publishers.
Examples:
Run server
go run server.go
Run consumer
go run consumer.go
Run publisher
go run publisher.go
package tasks
import (
"context"
"github.com/oarkflow/json"
"fmt"
"log"
"github.com/oarkflow/mq"
)
func Node1(ctx context.Context, task *mq.Task) mq.Result {
return mq.Result{Payload: task.Payload, TaskID: task.ID}
}
func Node2(ctx context.Context, task *mq.Task) mq.Result {
return mq.Result{Payload: task.Payload, TaskID: task.ID}
}
func Node3(ctx context.Context, task *mq.Task) mq.Result {
var user map[string]any
json.Unmarshal(task.Payload, &user)
age := int(user["age"].(float64))
status := "FAIL"
if age > 20 {
status = "PASS"
}
user["status"] = status
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload, Status: status}
}
func Node4(ctx context.Context, task *mq.Task) mq.Result {
var user map[string]any
json.Unmarshal(task.Payload, &user)
user["final"] = "D"
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload}
}
func Node5(ctx context.Context, task *mq.Task) mq.Result {
var user map[string]any
json.Unmarshal(task.Payload, &user)
user["salary"] = "E"
resultPayload, _ := json.Marshal(user)
return mq.Result{Payload: resultPayload}
}
func Node6(ctx context.Context, task *mq.Task) mq.Result {
var user map[string]any
json.Unmarshal(task.Payload, &user)
resultPayload, _ := json.Marshal(map[string]any{"storage": user})
return mq.Result{Payload: resultPayload}
}
func Callback(ctx context.Context, task mq.Result) mq.Result {
fmt.Println("Received task", task.TaskID, "Payload", string(task.Payload), task.Error, task.Topic)
return mq.Result{}
}
func NotifyResponse(ctx context.Context, result mq.Result) {
log.Printf("DAG Final response: TaskID: %s, Payload: %s, Topic: %s", result.TaskID, result.Payload, result.Topic)
}
Start Server
package main
import (
"context"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
b := mq.NewBroker(mq.WithCallback(tasks.Callback))
b.NewQueue("queue1")
b.NewQueue("queue2")
b.Start(context.Background())
}
Start Consumer
package main
import (
"context"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/examples/tasks"
)
func main() {
consumer1 := mq.NewConsumer("consumer-1", "queue1", tasks.Node1)
consumer2 := mq.NewConsumer("consumer-2", "queue2", tasks.Node2)
// consumer := mq.NewConsumer("consumer-1", mq.WithTLS(true, "./certs/server.crt", "./certs/server.key"))
go consumer1.Consume(context.Background())
consumer2.Consume(context.Background())
}
Publish tasks
package main
import (
"context"
"fmt"
"github.com/oarkflow/mq"
)
func main() {
payload := []byte(`{"message":"Message Publisher \n Task"}`)
task := mq.Task{
Payload: payload,
}
publisher := mq.NewPublisher("publish-1")
err := publisher.Publish(context.Background(), "queue1", task)
if err != nil {
panic(err)
}
fmt.Println("Async task published successfully")
payload = []byte(`{"message":"Fire-and-Forget \n Task"}`)
task = mq.Task{
Payload: payload,
}
result, err := publisher.Request(context.Background(), "queue1", task)
if err != nil {
panic(err)
}
fmt.Printf("Sync task published. Result: %v\n", string(result.Payload))
}
DAG (Directed Acyclic Graph)
In this package, you can use the DAG
feature to create a directed acyclic graph of tasks. The DAG
feature allows you to define a sequence of tasks that need to be executed in a specific order.
Example
package main
import (
"context"
"github.com/oarkflow/json"
"github.com/oarkflow/mq/consts"
"github.com/oarkflow/mq/examples/tasks"
"io"
"net/http"
"github.com/oarkflow/mq"
"github.com/oarkflow/mq/dag"
)
var (
d = dag.NewDAG(mq.WithSyncMode(false), mq.WithNotifyResponse(tasks.NotifyResponse))
// d = dag.NewDAG(mq.WithSyncMode(true), mq.WithTLS(true, "./certs/server.crt", "./certs/server.key"), mq.WithCAPath("./certs/ca.cert"))
)
func main() {
d.AddNode("A", tasks.Node1, true)
d.AddNode("B", tasks.Node2)
d.AddNode("C", tasks.Node3)
d.AddNode("D", tasks.Node4)
d.AddNode("E", tasks.Node5)
d.AddNode("F", tasks.Node6)
d.AddEdge("A", "B", dag.LoopEdge)
d.AddCondition("C", map[string]string{"PASS": "D", "FAIL": "E"})
d.AddEdge("B", "C")
d.AddEdge("D", "F")
d.AddEdge("E", "F")
http.HandleFunc("POST /publish", requestHandler("publish"))
http.HandleFunc("POST /request", requestHandler("request"))
err := d.Start(context.TODO(), ":8083")
if err != nil {
panic(err)
}
}
func requestHandler(requestType string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
var payload []byte
if r.Body != nil {
defer r.Body.Close()
var err error
payload, err = io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
} else {
http.Error(w, "Empty request body", http.StatusBadRequest)
return
}
ctx := context.Background()
if requestType == "request" {
ctx = mq.SetHeaders(ctx, map[string]string{consts.AwaitResponseKey: "true"})
}
// ctx = context.WithValue(ctx, "initial_node", "E")
rs := d.ProcessTask(ctx, payload)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(rs)
}
}
Middleware System
The DAG system supports a robust middleware system that allows you to add cross-cutting concerns like logging, validation, timing, and more to your DAG nodes.
Global Middlewares
Global middlewares apply to all nodes in the DAG:
flow.Use(LoggingMiddleware, ValidationMiddleware)
Node-Specific Middlewares
You can apply middlewares to specific nodes:
flow.UseNodeMiddlewares(
dag.NodeMiddleware{
Node: "process_a",
Middlewares: []mq.Handler{TimingMiddleware},
},
dag.NodeMiddleware{
Node: "process_b",
Middlewares: []mq.Handler{TimingMiddleware},
},
)
Middleware Example
// LoggingMiddleware logs the start and end of task processing
func LoggingMiddleware(ctx context.Context, task *mq.Task) mq.Result {
log.Printf("Middleware: Starting processing for node %s, task %s", task.Topic, task.ID)
// Return successful result to continue to next middleware/processor
return mq.Result{
Status: mq.Completed,
Ctx: ctx,
Payload: task.Payload,
}
}
// ValidationMiddleware validates the task payload
func ValidationMiddleware(ctx context.Context, task *mq.Task) mq.Result {
if len(task.Payload) == 0 {
return mq.Result{
Status: mq.Failed,
Error: fmt.Errorf("empty payload not allowed"),
Ctx: ctx,
}
}
return mq.Result{
Status: mq.Completed,
Ctx: ctx,
Payload: task.Payload,
}
}
Middleware Execution Order
Middlewares are executed in the following order:
- Global middlewares (in the order they were added)
- Node-specific middlewares (in the order they were added)
- The actual node processor
Each middleware can:
- Modify the context
- Modify the task payload
- Short-circuit processing by returning an error result
- Continue processing by returning a successful result
TODOS
- Backend for task persistence
- Task scheduling
Languages
Go
100%