mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-06 00:16:49 +08:00
feat: [wip] - Implement html node
This commit is contained in:
114
dag/v2/dag.go
114
dag/v2/dag.go
@@ -4,10 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/oarkflow/mq"
|
"github.com/oarkflow/mq"
|
||||||
|
"github.com/oarkflow/mq/consts"
|
||||||
"github.com/oarkflow/mq/storage"
|
"github.com/oarkflow/mq/storage"
|
||||||
"github.com/oarkflow/mq/storage/memory"
|
"github.com/oarkflow/mq/storage/memory"
|
||||||
)
|
)
|
||||||
@@ -76,10 +78,6 @@ func NewDAG(finalResultCallback func(taskID string, result Result)) *DAG {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *DAG) Validate(ctx context.Context) (string, error) {
|
|
||||||
return tm.parseInitialNode(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tm *DAG) parseInitialNode(ctx context.Context) (string, error) {
|
func (tm *DAG) parseInitialNode(ctx context.Context) (string, error) {
|
||||||
val := ctx.Value("initial_node")
|
val := ctx.Value("initial_node")
|
||||||
initialNode, ok := val.(string)
|
initialNode, ok := val.(string)
|
||||||
@@ -170,6 +168,26 @@ func (tm *DAG) GetPreviousNodes(key string) ([]*Node, error) {
|
|||||||
return predecessors, nil
|
return predecessors, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tm *DAG) ProcessTask(ctx context.Context, payload []byte) Result {
|
||||||
|
var taskID string
|
||||||
|
userCtx := UserContext(ctx)
|
||||||
|
if val := userCtx.Get("task_id"); val != "" {
|
||||||
|
taskID = val
|
||||||
|
} else {
|
||||||
|
taskID = mq.NewID()
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, "task_id", taskID)
|
||||||
|
resultCh := make(chan Result, 1)
|
||||||
|
manager := NewTaskManager(tm, resultCh)
|
||||||
|
tm.taskManager.Set(taskID, manager)
|
||||||
|
firstNode, err := tm.parseInitialNode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return Result{Error: err}
|
||||||
|
}
|
||||||
|
manager.ProcessTask(ctx, taskID, firstNode, payload)
|
||||||
|
return <-resultCh
|
||||||
|
}
|
||||||
|
|
||||||
func (tm *DAG) formHandler(w http.ResponseWriter, r *http.Request) {
|
func (tm *DAG) formHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == "GET" {
|
if r.Method == "GET" {
|
||||||
http.ServeFile(w, r, "webroot/form.html")
|
http.ServeFile(w, r, "webroot/form.html")
|
||||||
@@ -179,7 +197,8 @@ func (tm *DAG) formHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
age := r.FormValue("age")
|
age := r.FormValue("age")
|
||||||
gender := r.FormValue("gender")
|
gender := r.FormValue("gender")
|
||||||
taskID := mq.NewID()
|
taskID := mq.NewID()
|
||||||
manager := NewTaskManager(tm)
|
resultCh := make(chan Result, 1)
|
||||||
|
manager := NewTaskManager(tm, resultCh)
|
||||||
tm.taskManager.Set(taskID, manager)
|
tm.taskManager.Set(taskID, manager)
|
||||||
payload := fmt.Sprintf(`{"email": "%s", "age": "%s", "gender": "%s"}`, email, age, gender)
|
payload := fmt.Sprintf(`{"email": "%s", "age": "%s", "gender": "%s"}`, email, age, gender)
|
||||||
manager.ProcessTask(r.Context(), taskID, "NodeA", json.RawMessage(payload))
|
manager.ProcessTask(r.Context(), taskID, "NodeA", json.RawMessage(payload))
|
||||||
@@ -208,19 +227,92 @@ func (tm *DAG) taskStatusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
func (tm *DAG) Start(addr string) {
|
func (tm *DAG) Start(addr string) {
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) {
|
||||||
firstNode, err := tm.Validate(request.Context())
|
ctx, data, err := parse(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"message": "taskID is missing"}`, http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
node, _ := tm.nodes.Get(firstNode)
|
result := tm.ProcessTask(ctx, data)
|
||||||
if node.Type == Page {
|
if contentType, ok := result.Ctx.Value(consts.ContentType).(string); ok && contentType == consts.TypeHtml {
|
||||||
|
w.Header().Set(consts.ContentType, consts.TypeHtml)
|
||||||
|
w.Write(result.Data)
|
||||||
}
|
}
|
||||||
w.Write([]byte(firstNode))
|
|
||||||
})
|
})
|
||||||
http.HandleFunc("/form", tm.formHandler)
|
http.HandleFunc("/form", tm.formHandler)
|
||||||
http.HandleFunc("/result", tm.resultHandler)
|
http.HandleFunc("/result", tm.resultHandler)
|
||||||
http.HandleFunc("/task-result", tm.taskStatusHandler)
|
http.HandleFunc("/task-result", tm.taskStatusHandler)
|
||||||
http.ListenAndServe(addr, nil)
|
http.ListenAndServe(addr, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
Query map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) Get(key string) string {
|
||||||
|
if val, ok := ctx.Query[key]; ok {
|
||||||
|
switch val := val.(type) {
|
||||||
|
case []string:
|
||||||
|
return val[0]
|
||||||
|
case string:
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(r *http.Request) (context.Context, []byte, error) {
|
||||||
|
ctx := r.Context()
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, nil, err
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
userContext := &Context{Query: make(map[string]any)}
|
||||||
|
result := make(map[string]any)
|
||||||
|
queryParams := r.URL.Query()
|
||||||
|
for key, values := range queryParams {
|
||||||
|
if len(values) > 1 {
|
||||||
|
userContext.Query[key] = values // Handle multiple values
|
||||||
|
} else {
|
||||||
|
userContext.Query[key] = values[0] // Single value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, "UserContext", userContext)
|
||||||
|
contentType := r.Header.Get("Content-Type")
|
||||||
|
switch {
|
||||||
|
case contentType == "application/json":
|
||||||
|
if body == nil {
|
||||||
|
return ctx, nil, nil
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return ctx, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
case contentType == "application/x-www-form-urlencoded":
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return ctx, nil, err
|
||||||
|
}
|
||||||
|
result = make(map[string]any)
|
||||||
|
for key, values := range r.Form {
|
||||||
|
if len(values) > 1 {
|
||||||
|
result[key] = values
|
||||||
|
} else {
|
||||||
|
result[key] = values[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ctx, nil, nil
|
||||||
|
}
|
||||||
|
bt, err := json.Marshal(result)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, nil, err
|
||||||
|
}
|
||||||
|
return ctx, bt, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UserContext(ctx context.Context) *Context {
|
||||||
|
if userContext, ok := ctx.Value("UserContext").(*Context); ok {
|
||||||
|
return userContext
|
||||||
|
}
|
||||||
|
return &Context{Query: make(map[string]any)}
|
||||||
|
}
|
||||||
|
@@ -28,24 +28,36 @@ type nodeResult struct {
|
|||||||
|
|
||||||
type TaskManager struct {
|
type TaskManager struct {
|
||||||
taskStates map[string]*TaskState
|
taskStates map[string]*TaskState
|
||||||
|
currentNode string
|
||||||
dag *DAG
|
dag *DAG
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
taskQueue chan taskExecution
|
taskQueue chan *Task
|
||||||
resultQueue chan nodeResult
|
resultQueue chan nodeResult
|
||||||
|
resultCh chan Result
|
||||||
}
|
}
|
||||||
|
|
||||||
type taskExecution struct {
|
type Task struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
taskID string
|
taskID string
|
||||||
nodeID string
|
nodeID string
|
||||||
payload json.RawMessage
|
payload json.RawMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTaskManager(dag *DAG) *TaskManager {
|
func NewTask(ctx context.Context, taskID, nodeID string, payload json.RawMessage) *Task {
|
||||||
|
return &Task{
|
||||||
|
ctx: ctx,
|
||||||
|
taskID: taskID,
|
||||||
|
nodeID: nodeID,
|
||||||
|
payload: payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTaskManager(dag *DAG, resultCh chan Result) *TaskManager {
|
||||||
tm := &TaskManager{
|
tm := &TaskManager{
|
||||||
taskStates: make(map[string]*TaskState),
|
taskStates: make(map[string]*TaskState),
|
||||||
taskQueue: make(chan taskExecution, 100),
|
taskQueue: make(chan *Task, 100),
|
||||||
resultQueue: make(chan nodeResult, 100),
|
resultQueue: make(chan nodeResult, 100),
|
||||||
|
resultCh: resultCh,
|
||||||
dag: dag,
|
dag: dag,
|
||||||
}
|
}
|
||||||
go tm.Run()
|
go tm.Run()
|
||||||
@@ -57,7 +69,7 @@ func (tm *TaskManager) ProcessTask(ctx context.Context, taskID, startNode string
|
|||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
tm.taskStates[startNode] = newTaskState(startNode)
|
tm.taskStates[startNode] = newTaskState(startNode)
|
||||||
tm.mu.Unlock()
|
tm.mu.Unlock()
|
||||||
tm.taskQueue <- taskExecution{taskID: taskID, nodeID: startNode, payload: payload, ctx: ctx}
|
tm.taskQueue <- NewTask(ctx, taskID, startNode, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTaskState(nodeID string) *TaskState {
|
func newTaskState(nodeID string) *TaskState {
|
||||||
@@ -77,7 +89,7 @@ func (tm *TaskManager) Run() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tm *TaskManager) processNode(exec taskExecution) {
|
func (tm *TaskManager) processNode(exec *Task) {
|
||||||
node, exists := tm.dag.nodes.Get(exec.nodeID)
|
node, exists := tm.dag.nodes.Get(exec.nodeID)
|
||||||
if !exists {
|
if !exists {
|
||||||
fmt.Printf("Node %s does not exist\n", exec.nodeID)
|
fmt.Printf("Node %s does not exist\n", exec.nodeID)
|
||||||
@@ -92,13 +104,20 @@ func (tm *TaskManager) processNode(exec taskExecution) {
|
|||||||
}
|
}
|
||||||
state.Status = StatusProcessing
|
state.Status = StatusProcessing
|
||||||
state.UpdatedAt = time.Now()
|
state.UpdatedAt = time.Now()
|
||||||
result := node.Handler(context.Background(), exec.payload)
|
tm.currentNode = exec.nodeID
|
||||||
|
result := node.Handler(exec.ctx, exec.payload)
|
||||||
state.UpdatedAt = time.Now()
|
state.UpdatedAt = time.Now()
|
||||||
state.Result = result
|
state.Result = result
|
||||||
state.Status = result.Status
|
if result.Ctx == nil {
|
||||||
if result.Status == StatusFailed {
|
result.Ctx = exec.ctx
|
||||||
fmt.Printf("Task %s failed at node %s: %v\n", exec.taskID, exec.nodeID, result.Error)
|
}
|
||||||
tm.processFinalResult(exec.taskID, state)
|
if result.Error != nil {
|
||||||
|
state.Status = StatusFailed
|
||||||
|
} else {
|
||||||
|
state.Status = StatusCompleted
|
||||||
|
}
|
||||||
|
if node.Type == Page {
|
||||||
|
tm.resultCh <- result
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tm.resultQueue <- nodeResult{taskID: exec.taskID, nodeID: exec.nodeID, result: result, ctx: exec.ctx}
|
tm.resultQueue <- nodeResult{taskID: exec.taskID, nodeID: exec.nodeID, result: result, ctx: exec.ctx}
|
||||||
@@ -117,16 +136,7 @@ func (tm *TaskManager) onNodeCompleted(nodeResult nodeResult) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(node.Edges) > 0 {
|
if nodeResult.result.Error != nil || len(node.Edges) == 0 {
|
||||||
for _, edge := range node.Edges {
|
|
||||||
tm.mu.Lock()
|
|
||||||
if _, exists := tm.taskStates[edge.To.ID]; !exists {
|
|
||||||
tm.taskStates[edge.To.ID] = newTaskState(edge.To.ID)
|
|
||||||
}
|
|
||||||
tm.mu.Unlock()
|
|
||||||
tm.taskQueue <- taskExecution{taskID: nodeResult.taskID, nodeID: edge.To.ID, payload: nodeResult.result.Data, ctx: nodeResult.ctx}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
parentNodes, err := tm.dag.GetPreviousNodes(nodeResult.nodeID)
|
parentNodes, err := tm.dag.GetPreviousNodes(nodeResult.nodeID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, parentNode := range parentNodes {
|
for _, parentNode := range parentNodes {
|
||||||
@@ -145,6 +155,15 @@ func (tm *TaskManager) onNodeCompleted(nodeResult nodeResult) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, edge := range node.Edges {
|
||||||
|
tm.mu.Lock()
|
||||||
|
if _, exists := tm.taskStates[edge.To.ID]; !exists {
|
||||||
|
tm.taskStates[edge.To.ID] = newTaskState(edge.To.ID)
|
||||||
|
}
|
||||||
|
tm.mu.Unlock()
|
||||||
|
tm.taskQueue <- NewTask(nodeResult.ctx, nodeResult.taskID, edge.To.ID, nodeResult.result.Data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,11 +7,12 @@ import (
|
|||||||
|
|
||||||
"github.com/oarkflow/jet"
|
"github.com/oarkflow/jet"
|
||||||
|
|
||||||
|
"github.com/oarkflow/mq/consts"
|
||||||
v2 "github.com/oarkflow/mq/dag/v2"
|
v2 "github.com/oarkflow/mq/dag/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Form(ctx context.Context, payload json.RawMessage) v2.Result {
|
func Form(ctx context.Context, payload json.RawMessage) v2.Result {
|
||||||
template := []byte(`
|
template := `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -21,7 +22,7 @@ func Form(ctx context.Context, payload json.RawMessage) v2.Result {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Enter Your Information</h1>
|
<h1>Enter Your Information</h1>
|
||||||
<form action="/form" method="POST">
|
<form action="/?task_id={{task_id}}&next=true" method="POST">
|
||||||
<label for="email">Email:</label><br>
|
<label for="email">Email:</label><br>
|
||||||
<input type="email" id="email" name="email" value="s.baniya.np@gmail.com" required><br><br>
|
<input type="email" id="email" name="email" value="s.baniya.np@gmail.com" required><br><br>
|
||||||
|
|
||||||
@@ -40,55 +41,63 @@ func Form(ctx context.Context, payload json.RawMessage) v2.Result {
|
|||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
`)
|
`
|
||||||
return v2.Result{Data: template, Status: v2.StatusCompleted}
|
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||||
|
rs, err := parser.ParseTemplate(template, map[string]any{
|
||||||
|
"task_id": ctx.Value("task_id"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return v2.Result{Error: err, Ctx: ctx}
|
||||||
|
}
|
||||||
|
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||||
|
return v2.Result{Data: []byte(rs), Ctx: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NodeA(ctx context.Context, payload json.RawMessage) v2.Result {
|
func NodeA(ctx context.Context, payload json.RawMessage) v2.Result {
|
||||||
var data map[string]any
|
var data map[string]any
|
||||||
if err := json.Unmarshal(payload, &data); err != nil {
|
if err := json.Unmarshal(payload, &data); err != nil {
|
||||||
return v2.Result{Error: err, Status: v2.StatusFailed}
|
return v2.Result{Error: err}
|
||||||
}
|
}
|
||||||
data["allowed_voting"] = data["age"] == "18"
|
data["allowed_voting"] = data["age"] == "18"
|
||||||
updatedPayload, _ := json.Marshal(data)
|
updatedPayload, _ := json.Marshal(data)
|
||||||
return v2.Result{Data: updatedPayload, Status: v2.StatusCompleted}
|
return v2.Result{Data: updatedPayload, Ctx: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NodeB(ctx context.Context, payload json.RawMessage) v2.Result {
|
func NodeB(ctx context.Context, payload json.RawMessage) v2.Result {
|
||||||
var data map[string]any
|
var data map[string]any
|
||||||
if err := json.Unmarshal(payload, &data); err != nil {
|
if err := json.Unmarshal(payload, &data); err != nil {
|
||||||
return v2.Result{Error: err, Status: v2.StatusFailed}
|
return v2.Result{Error: err, Ctx: ctx}
|
||||||
}
|
}
|
||||||
data["female_voter"] = data["gender"] == "female"
|
data["female_voter"] = data["gender"] == "female"
|
||||||
updatedPayload, _ := json.Marshal(data)
|
updatedPayload, _ := json.Marshal(data)
|
||||||
return v2.Result{Data: updatedPayload, Status: v2.StatusCompleted}
|
return v2.Result{Data: updatedPayload, Ctx: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NodeC(ctx context.Context, payload json.RawMessage) v2.Result {
|
func NodeC(ctx context.Context, payload json.RawMessage) v2.Result {
|
||||||
var data map[string]any
|
var data map[string]any
|
||||||
if err := json.Unmarshal(payload, &data); err != nil {
|
if err := json.Unmarshal(payload, &data); err != nil {
|
||||||
return v2.Result{Error: err, Status: v2.StatusFailed}
|
return v2.Result{Error: err, Ctx: ctx}
|
||||||
}
|
}
|
||||||
data["voted"] = true
|
data["voted"] = true
|
||||||
updatedPayload, _ := json.Marshal(data)
|
updatedPayload, _ := json.Marshal(data)
|
||||||
return v2.Result{Data: updatedPayload, Status: v2.StatusCompleted}
|
return v2.Result{Data: updatedPayload, Ctx: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Result(ctx context.Context, payload json.RawMessage) v2.Result {
|
func Result(ctx context.Context, payload json.RawMessage) v2.Result {
|
||||||
var data map[string]any
|
var data map[string]any
|
||||||
if err := json.Unmarshal(payload, &data); err != nil {
|
if err := json.Unmarshal(payload, &data); err != nil {
|
||||||
return v2.Result{Error: err, Status: v2.StatusFailed}
|
return v2.Result{Error: err, Ctx: ctx}
|
||||||
}
|
}
|
||||||
if templateFile, ok := data["html_content"].(string); ok {
|
if templateFile, ok := data["html_content"].(string); ok {
|
||||||
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
parser := jet.NewWithMemory(jet.WithDelims("{{", "}}"))
|
||||||
rs, err := parser.ParseTemplate(templateFile, data)
|
rs, err := parser.ParseTemplate(templateFile, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return v2.Result{Error: err, Status: v2.StatusFailed}
|
return v2.Result{Error: err, Ctx: ctx}
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, "Content-Type", "text/html; charset/utf-8")
|
ctx = context.WithValue(ctx, consts.ContentType, consts.TypeHtml)
|
||||||
return v2.Result{Data: []byte(rs), Status: v2.StatusCompleted, Ctx: ctx}
|
return v2.Result{Data: []byte(rs), Ctx: ctx}
|
||||||
}
|
}
|
||||||
return v2.Result{Data: payload, Status: v2.StatusCompleted}
|
return v2.Result{Data: payload, Ctx: ctx}
|
||||||
}
|
}
|
||||||
|
|
||||||
func notify(taskID string, result v2.Result) {
|
func notify(taskID string, result v2.Result) {
|
||||||
|
Reference in New Issue
Block a user