feat: [wip] - Implement html node

This commit is contained in:
sujit
2024-11-18 19:01:15 +05:45
parent ce3a063bb8
commit 3aaffe7d07
3 changed files with 167 additions and 47 deletions

View File

@@ -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)}
}

View File

@@ -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)
} }
} }

View File

@@ -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) {