Files
orbit/sdk/go/cmd/orbit-workflow-load/measure.go

328 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"sync"
"time"
orbit "git.yuntuo.hk/k2safe/orbit/sdk/go"
)
// workflowPayload 是两个并行分支共享的固定输入;不含凭据或外部地址。
type workflowPayload struct {
RunID string `json:"run_id"` // RunID 隔离本次测量,不作为业务幂等保证。
Sequence int `json:"sequence"` // Sequence 从0开始的实例序号,用于发现错配和重复调用。
}
func handlerName(runID string) string { return "load.workflow." + runID }
func definitionKey(runID string) string { return "orbit-workflow-load-" + runID }
func requestKey(runID string, sequence int) string {
return fmt.Sprintf("orbit-workflow-%s-%06d", runID, sequence)
}
func businessKey(runID string, sequence int) string {
return fmt.Sprintf("orbit-workflow-%s-%06d", runID, sequence)
}
type workflowAPI interface {
GetQueue(context.Context, string) (orbit.Queue, error)
CreateWorkflowDefinition(context.Context, string, orbit.CreateWorkflowDefinitionRequest) (orbit.WorkflowDefinitionResult, error)
PublishWorkflowVersion(context.Context, string, string, orbit.PublishWorkflowVersionRequest) (orbit.WorkflowVersionResult, error)
StartWorkflow(context.Context, string, orbit.StartWorkflowRequest) (orbit.WorkflowStartResult, error)
GetWorkflow(context.Context, string) (orbit.WorkflowInstance, error)
GetWorkflowSteps(context.Context, string) (orbit.WorkflowSteps, error)
GetTask(context.Context, string) (orbit.Task, error)
ListTaskAttempts(context.Context, string, orbit.HistoryPageRequest) (orbit.AttemptPage, error)
GetTaskSource(context.Context, string) (orbit.TaskSource, error)
}
type workflowAdmission struct {
sequence int
instance orbit.WorkflowInstance
duration time.Duration
code string
}
type workflowVerification struct {
sequence int
duration time.Duration
code string
tasks []verifiedTask
}
func workflowSpec(queue, handler string, timeout time.Duration) orbit.WorkflowSpecification {
typ := orbit.WorkflowType{Kind: "OBJECT", Fields: map[string]orbit.WorkflowType{"run_id": {Kind: "STRING"}, "sequence": {Kind: "NUMBER"}}}
task := orbit.WorkflowTask{QueueID: queue, Handler: handler, HandlerVersion: "v1", PayloadVersion: "v1", InputType: typ, Input: orbit.WorkflowRef("input"), ResultType: typ, TimeoutMS: uint64(timeout.Milliseconds()), Retry: orbit.RetryPolicy{MaxAttempts: 1, AttemptTimeoutMS: uint64(timeout.Milliseconds())}, RecoverySafety: orbit.RecoveryReconcileOnly}
parallel := orbit.WorkflowParallel{Branches: []orbit.WorkflowParallelBranch{
{Name: "left", Body: orbit.WorkflowBlock{Steps: []orbit.WorkflowStep{{Key: "left", Kind: "TASK", Task: &task}}, OutputType: typ, Output: orbit.WorkflowRef("left")}},
{Name: "right", Body: orbit.WorkflowBlock{Steps: []orbit.WorkflowStep{{Key: "right", Kind: "TASK", Task: &task}}, OutputType: typ, Output: orbit.WorkflowRef("right")}},
}}
output := orbit.WorkflowType{Kind: "OBJECT", Fields: map[string]orbit.WorkflowType{"left": typ, "right": typ}}
return orbit.WorkflowSpecification{SchemaVersion: 1, InputType: typ, TimeoutMS: uint64(timeout.Milliseconds()), FailurePolicy: "REVIEW", Body: orbit.WorkflowBlock{Steps: []orbit.WorkflowStep{{Key: "pair", Kind: "PARALLEL", Parallel: &parallel}}, OutputType: output, Output: orbit.WorkflowRef("pair")}}
}
func measure(parent context.Context, cfg config, api *orbit.Client, runID string) workflowReport {
return measureClient(parent, cfg, api, runID)
}
func measureClient(parent context.Context, cfg config, api *orbit.Client, runID string) workflowReport {
r := newReport(cfg, runID)
ctx, cancel := context.WithTimeout(parent, cfg.RunTimeout)
defer cancel()
requestCtx, stop := context.WithTimeout(ctx, cfg.Timeout)
queue, err := api.GetQueue(requestCtx, cfg.QueueID)
stop()
if err != nil {
r.Errors["PREFLIGHT_"+classify(err)]++
return r.finish()
}
r.Queue = &queue
if queue.ID != cfg.QueueID || !validID(queue.NamespaceID) || queue.Paused || queue.MaxConcurrency == 0 || queue.RatePerSecond == 0 {
r.Errors["QUEUE_UNAVAILABLE"]++
return r.finish()
}
handler := handlerName(runID)
spec := workflowSpec(cfg.QueueID, handler, cfg.RunTimeout)
requestCtx, stop = context.WithTimeout(ctx, cfg.Timeout)
definition, err := api.CreateWorkflowDefinition(requestCtx, definitionKey(runID), orbit.CreateWorkflowDefinitionRequest{Name: definitionKey(runID), Description: "bounded parallel workflow verification"})
stop()
if err != nil {
r.Errors["CREATE_"+classify(err)]++
return r.finish()
}
r.DefinitionID = definition.Definition.ID
if !validID(r.DefinitionID) || definition.Deduplicated || definition.Definition.NamespaceID != queue.NamespaceID || definition.Definition.Name != definitionKey(runID) {
r.Errors["INVALID_DEFINITION_RECEIPT"]++
return r.finish()
}
requestCtx, stop = context.WithTimeout(ctx, cfg.Timeout)
version, err := api.PublishWorkflowVersion(requestCtx, r.DefinitionID, runID, orbit.PublishWorkflowVersionRequest{Version: "v1", Specification: spec, Reason: "bounded parallel workflow verification"})
stop()
if err != nil {
r.Errors["PUBLISH_"+classify(err)]++
return r.finish()
}
r.VersionID = version.Version.ID
if !validID(r.VersionID) || version.Version.Version != "v1" || version.Version.DefinitionID != r.DefinitionID || version.Deduplicated || version.Version.NamespaceID != queue.NamespaceID || !sameSpecification(version.Version.Specification, spec) {
r.Errors["INVALID_VERSION_RECEIPT"]++
return r.finish()
}
log := newInvocationLog(cfg.Requests)
workers := make([]*orbit.Worker, 0, cfg.Workers)
for i := 0; i < cfg.Workers; i++ {
worker, e := orbit.NewWorker(api, orbit.WorkerOptions{QueueID: cfg.QueueID, Capacity: uint32(cfg.Capacity), ShutdownTimeout: 5 * time.Second, Handlers: []orbit.WorkerHandler{log.handler(ctx, cfg, runID)}})
if e != nil {
r.Errors["WORKER_CONFIG"]++
return r.finish()
}
workers = append(workers, worker)
}
workerCtx, stopWorkers := context.WithCancel(ctx)
done := make(chan error, len(workers))
for _, worker := range workers {
go func(w *orbit.Worker) {
e := w.Run(workerCtx)
if workerCtx.Err() == nil {
cancel()
}
done <- e
}(worker)
}
admissions := submitWorkflows(ctx, cfg, api, runID, r.DefinitionID, r.VersionID, queue.NamespaceID)
r.Attempted = len(admissions)
r.NotSubmitted = cfg.Requests - r.Attempted
var accepted []workflowAdmission
var admissionDurations []time.Duration
seenInstances := map[string]bool{}
for _, a := range admissions {
admissionDurations = append(admissionDurations, a.duration)
if a.code == "" && seenInstances[a.instance.ID] {
a.code = "DUPLICATE_INSTANCE"
}
if a.code == "" {
seenInstances[a.instance.ID] = true
}
r.Instances = append(r.Instances, instanceReport{Sequence: a.sequence, Key: requestKey(runID, a.sequence), ID: a.instance.ID, Code: a.code})
if a.code != "" {
r.AdmissionErrors++
r.Errors["ADMISSION_"+a.code]++
continue
}
accepted = append(accepted, a)
}
r.Accepted = len(accepted)
r.Admission = distributionOf(admissionDurations)
r.AdmissionFinishedAt = time.Now().UTC()
verified := verifyWorkflows(ctx, cfg, api, runID, r.DefinitionID, accepted)
var completions []time.Duration
var proven []verifiedTask
for _, v := range verified {
if v.code != "" {
r.Errors[v.code]++
continue
}
r.VerifiedSuccess++
proven = append(proven, v.tasks...)
for i := range r.Instances {
if r.Instances[i].Sequence == v.sequence {
r.Instances[i].Verified = true
break
}
}
completions = append(completions, v.duration)
}
if ctx.Err() != nil {
r.Errors["RUN_"+classify(ctx.Err())]++
}
stopWorkers()
for range workers {
if e := <-done; e != nil {
r.Errors["WORKER_STOP"]++
}
}
for _, worker := range workers {
stats := worker.Stats()
r.WorkerStarted += stats.Started
r.Reconnects += stats.Reconnects
r.Renewals += stats.RenewalsConfirmed
}
log.summarize(&r, proven)
r.Completion = distributionOf(completions)
return r.finish()
}
func submitWorkflows(ctx context.Context, cfg config, api workflowAPI, runID, definitionID, versionID, namespace string) []workflowAdmission {
jobs := make(chan int)
out := make(chan workflowAdmission, cfg.Requests)
var wg sync.WaitGroup
for i := 0; i < cfg.Concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for sequence := range jobs {
if ctx.Err() != nil {
continue
}
payload, _ := json.Marshal(workflowPayload{RunID: runID, Sequence: sequence})
started := time.Now()
call, stop := context.WithTimeout(ctx, cfg.Timeout)
result, err := api.StartWorkflow(call, requestKey(runID, sequence), orbit.StartWorkflowRequest{DefinitionID: definitionID, Version: "v1", Input: payload, BusinessKey: businessKey(runID, sequence)})
stop()
a := workflowAdmission{sequence: sequence, instance: result.Instance, duration: time.Since(started)}
if err != nil {
a.code = classify(err)
} else if result.Deduplicated || !matchesInstance(result.Instance, cfg, runID, sequence, definitionID, versionID, namespace) {
a.code = "INVALID_RECEIPT"
}
out <- a
}
}()
}
feed:
for sequence := 0; sequence < cfg.Requests; sequence++ {
select {
case <-ctx.Done():
break feed
case jobs <- sequence:
}
}
close(jobs)
wg.Wait()
close(out)
results := make([]workflowAdmission, 0, cfg.Requests)
for result := range out {
results = append(results, result)
}
return results
}
func verifyWorkflows(ctx context.Context, cfg config, api workflowAPI, runID, definitionID string, admissions []workflowAdmission) []workflowVerification {
jobs := make(chan workflowAdmission, len(admissions))
for _, admission := range admissions {
jobs <- admission
}
close(jobs)
out := make(chan workflowVerification, len(admissions))
var wg sync.WaitGroup
for i := 0; i < cfg.Concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for admission := range jobs {
duration, tasks, code := verifyWorkflow(ctx, cfg, api, runID, definitionID, admission)
out <- workflowVerification{sequence: admission.sequence, duration: duration, tasks: tasks, code: code}
}
}()
}
wg.Wait()
close(out)
results := make([]workflowVerification, 0, len(admissions))
for result := range out {
results = append(results, result)
}
return results
}
func verifyWorkflow(ctx context.Context, cfg config, api workflowAPI, runID, definitionID string, admission workflowAdmission) (time.Duration, []verifiedTask, string) {
for {
if ctx.Err() != nil {
return 0, nil, "VERIFY_" + classify(ctx.Err())
}
call, stop := context.WithTimeout(ctx, cfg.Timeout)
instance, err := api.GetWorkflow(call, admission.instance.ID)
stop()
if err != nil {
return 0, nil, "READ_" + classify(err)
}
if instance.ID != admission.instance.ID || !matchesInstance(instance, cfg, runID, admission.sequence, definitionID, admission.instance.DefinitionVersionID, admission.instance.NamespaceID) || !instance.CreatedAt.Equal(admission.instance.CreatedAt) || !instance.DeadlineAt.Equal(admission.instance.DeadlineAt) {
return 0, nil, "WORKFLOW_MISMATCH"
}
switch instance.State {
case "SUCCEEDED":
if instance.CompletedAt == nil || instance.CreatedAt.IsZero() || instance.CompletedAt.Before(instance.CreatedAt) {
return 0, nil, "COMPLETION_MISMATCH"
}
call, stop = context.WithTimeout(ctx, cfg.Timeout)
steps, e := api.GetWorkflowSteps(call, instance.ID)
stop()
if e != nil {
return 0, nil, "STEPS_" + classify(e)
}
tasks, code := verifySteps(ctx, cfg, api, instance, runID, admission.sequence, steps)
if code != "" {
return 0, nil, code
}
if !matchesOutput(instance.Output, runID, admission.sequence) {
return 0, nil, "OUTPUT_MISMATCH"
}
return instance.CompletedAt.Sub(instance.CreatedAt), tasks, ""
case "NEEDS_REVIEW", "FAILED", "CANCELLED":
return 0, nil, "WORKFLOW_" + instance.State
case "RUNNING", "WAITING", "PAUSED", "PENDING":
default:
return 0, nil, "UNKNOWN_WORKFLOW_STATE"
}
timer := time.NewTimer(cfg.PollInterval)
select {
case <-ctx.Done():
timer.Stop()
return 0, nil, "VERIFY_" + classify(ctx.Err())
case <-timer.C:
}
}
}
func sameSpecification(a, b orbit.WorkflowSpecification) bool {
left, e := json.Marshal(a)
right, e2 := json.Marshal(b)
return e == nil && e2 == nil && bytes.Equal(left, right)
}
func matchesInstance(i orbit.WorkflowInstance, c config, runID string, sequence int, definitionID, versionID, namespace string) bool {
p, ok := decodePayload(i.Input)
return validID(i.ID) && i.NamespaceID == namespace && i.DefinitionID == definitionID && i.DefinitionVersionID == versionID && i.Version == "v1" && i.BusinessKey == businessKey(runID, sequence) && ok && p == (workflowPayload{RunID: runID, Sequence: sequence}) && !i.CreatedAt.IsZero() && i.DeadlineAt.Equal(i.CreatedAt.Add(time.Duration(c.RunTimeout.Milliseconds())*time.Millisecond)) && !i.Paused && i.CancelRequestedAt == nil && i.StopKind == nil && i.StopRequestedAt == nil
}