flexiq 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +302 -0
- data/lib/flexiq/backoff.rb +21 -0
- data/lib/flexiq/batch_item_result.rb +21 -0
- data/lib/flexiq/cbor/decoder.rb +169 -0
- data/lib/flexiq/cbor/encoder.rb +112 -0
- data/lib/flexiq/client.rb +196 -0
- data/lib/flexiq/deadline.rb +24 -0
- data/lib/flexiq/debounce.rb +20 -0
- data/lib/flexiq/edge_condition.rb +24 -0
- data/lib/flexiq/enqueue_options.rb +38 -0
- data/lib/flexiq/enqueue_request.rb +25 -0
- data/lib/flexiq/enqueue_result.rb +13 -0
- data/lib/flexiq/errors.rb +21 -0
- data/lib/flexiq/job.rb +60 -0
- data/lib/flexiq/job_page.rb +19 -0
- data/lib/flexiq/job_status.rb +41 -0
- data/lib/flexiq/job_transition.rb +25 -0
- data/lib/flexiq/payload.rb +55 -0
- data/lib/flexiq/queue_stats.rb +12 -0
- data/lib/flexiq/reason.rb +52 -0
- data/lib/flexiq/rpc_error.rb +117 -0
- data/lib/flexiq/task_error.rb +50 -0
- data/lib/flexiq/transition_kind.rb +27 -0
- data/lib/flexiq/transport.rb +216 -0
- data/lib/flexiq/version.rb +6 -0
- data/lib/flexiq/watch/by_ids.rb +33 -0
- data/lib/flexiq/watch/by_queue.rb +33 -0
- data/lib/flexiq/watch.rb +132 -0
- data/lib/flexiq/watch_items.rb +13 -0
- data/lib/flexiq/wire/bytes.rb +23 -0
- data/lib/flexiq/wire/duration.rb +41 -0
- data/lib/flexiq/wire/int64.rb +20 -0
- data/lib/flexiq/wire/path.rb +20 -0
- data/lib/flexiq/wire/sse.rb +91 -0
- data/lib/flexiq/wire/timestamp.rb +27 -0
- data/lib/flexiq/workflow_constructs.rb +66 -0
- data/lib/flexiq/workflow_edge.rb +21 -0
- data/lib/flexiq/workflow_graph.rb +50 -0
- data/lib/flexiq/workflow_node.rb +54 -0
- data/lib/flexiq/workflow_node_state.rb +17 -0
- data/lib/flexiq/workflow_node_status.rb +31 -0
- data/lib/flexiq/workflow_run.rb +24 -0
- data/lib/flexiq/workflow_run_view.rb +19 -0
- data/lib/flexiq/workflow_state.rb +31 -0
- data/lib/flexiq.rb +53 -0
- metadata +92 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
module Wire
|
|
5
|
+
# proto3 JSON `google.protobuf.Duration`: seconds with an `s` suffix, as in `"30s"` or `"1.500s"`.
|
|
6
|
+
module Duration
|
|
7
|
+
NANOS_PER_SECOND = 1_000_000_000
|
|
8
|
+
PATTERN = /\A(-)?(\d+)(?:\.(\d{1,9}))?s\z/
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
# Seconds (Integer, Float or Rational) to the wire string; nanosecond precision.
|
|
13
|
+
def dump(seconds)
|
|
14
|
+
raise ArgumentError, "a duration must be Numeric, got #{seconds.class}" unless seconds.is_a?(Numeric)
|
|
15
|
+
raise ArgumentError, "a duration must be finite" if seconds.is_a?(Float) && !seconds.finite?
|
|
16
|
+
raise ArgumentError, "a duration cannot be negative: #{seconds}" if seconds.negative?
|
|
17
|
+
|
|
18
|
+
whole, nanos = (seconds.to_r * NANOS_PER_SECOND).round.divmod(NANOS_PER_SECOND)
|
|
19
|
+
"#{whole}#{fraction(nanos)}s"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The wire string to seconds as a Rational, so no precision is lost; nil when unreadable.
|
|
23
|
+
def load(text)
|
|
24
|
+
match = PATTERN.match(text.to_s) or return nil
|
|
25
|
+
|
|
26
|
+
magnitude = Integer(match[2], 10) + Rational(Integer((match[3] || "0").ljust(9, "0"), 10), NANOS_PER_SECOND)
|
|
27
|
+
match[1] ? -magnitude : magnitude
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Same digit groups the server writes: none, millis, micros or nanos.
|
|
31
|
+
def fraction(nanos)
|
|
32
|
+
if nanos.zero? then ""
|
|
33
|
+
elsif (nanos % 1_000_000).zero? then format(".%03d", nanos / 1_000_000)
|
|
34
|
+
elsif (nanos % 1_000).zero? then format(".%06d", nanos / 1_000)
|
|
35
|
+
else format(".%09d", nanos)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
private_class_method :fraction
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
module Wire
|
|
5
|
+
# proto3 JSON writes 64-bit integers as strings, since a JSON number is a double.
|
|
6
|
+
module Int64
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def dump(value) = Integer(value).to_s
|
|
10
|
+
|
|
11
|
+
# Accepts the string form and, leniently, a bare number; nil when unreadable.
|
|
12
|
+
def load(value)
|
|
13
|
+
case value
|
|
14
|
+
when Integer then value
|
|
15
|
+
when String then Integer(value, 10, exception: false)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
module Wire
|
|
5
|
+
# Builds request paths from caller-supplied names.
|
|
6
|
+
module Path
|
|
7
|
+
UNRESERVED = /[^A-Za-z0-9\-._~]/
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Percent-encodes one path segment, so an id or queue name cannot add segments or a verb.
|
|
12
|
+
def segment(value)
|
|
13
|
+
text = value.to_s
|
|
14
|
+
raise ArgumentError, "a path segment cannot be empty" if text.empty?
|
|
15
|
+
|
|
16
|
+
text.b.gsub(UNRESERVED) { |char| format("%%%02X", char.ord) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
module Wire
|
|
5
|
+
# WHATWG `text/event-stream`, the framing of a watch.
|
|
6
|
+
module SSE
|
|
7
|
+
# `id`: this event's own id field, or nil. `type`: "message" unless `event` set one.
|
|
8
|
+
Event = Data.define(:id, :type, :data)
|
|
9
|
+
|
|
10
|
+
# Incremental parser. Holds partial lines across chunks; skips comments and `retry`;
|
|
11
|
+
# an event never ended by a blank line is dropped.
|
|
12
|
+
class Parser
|
|
13
|
+
LINE_BREAK = /[\r\n]/
|
|
14
|
+
CR = 13
|
|
15
|
+
LF = 10
|
|
16
|
+
private_constant :LINE_BREAK, :CR, :LF
|
|
17
|
+
|
|
18
|
+
# Bound on an unfinished line and on one event's data. A watch item is a few hundred
|
|
19
|
+
# bytes; a peer that never ends a line or event must not grow memory without bound.
|
|
20
|
+
MAX_BYTES = 1 << 20
|
|
21
|
+
|
|
22
|
+
def initialize(max_bytes: MAX_BYTES)
|
|
23
|
+
@max_bytes = max_bytes
|
|
24
|
+
@buffer = "".b
|
|
25
|
+
@after_cr = false
|
|
26
|
+
reset_event
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Yields each event the chunk completes.
|
|
30
|
+
def feed(chunk, &)
|
|
31
|
+
@buffer << chunk.b
|
|
32
|
+
drop_lf_after_cr
|
|
33
|
+
while (index = @buffer.index(LINE_BREAK))
|
|
34
|
+
line = @buffer.byteslice(0, index)
|
|
35
|
+
@after_cr = @buffer.getbyte(index) == CR
|
|
36
|
+
@buffer = @buffer.byteslice((index + 1)..)
|
|
37
|
+
drop_lf_after_cr
|
|
38
|
+
process(line, &)
|
|
39
|
+
end
|
|
40
|
+
limit!(@buffer, "line")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# CR ends a line at once; skip the LF of a CRLF split across chunks.
|
|
46
|
+
def drop_lf_after_cr
|
|
47
|
+
return if !@after_cr || @buffer.empty?
|
|
48
|
+
|
|
49
|
+
@buffer = @buffer.byteslice(1..) if @buffer.getbyte(0) == LF
|
|
50
|
+
@after_cr = false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def process(line, &)
|
|
54
|
+
return dispatch(&) if line.empty?
|
|
55
|
+
return if line.start_with?(":")
|
|
56
|
+
|
|
57
|
+
field, value = line.split(":", 2)
|
|
58
|
+
value = value.nil? ? "".b : value.delete_prefix(" ")
|
|
59
|
+
case field
|
|
60
|
+
when "data" then limit!(@data << value << "\n", "event")
|
|
61
|
+
when "event" then @type = value
|
|
62
|
+
when "id" then @id = value unless value.include?("\0")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def limit!(bytes, what)
|
|
67
|
+
return if bytes.bytesize <= @max_bytes
|
|
68
|
+
|
|
69
|
+
raise TransportError, "an event-stream #{what} passed #{@max_bytes} bytes unfinished"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def dispatch
|
|
73
|
+
unless @data.empty?
|
|
74
|
+
type = @type.nil? || @type.empty? ? "message" : text(@type)
|
|
75
|
+
yield Event.new(id: @id && text(@id), type: type, data: text(@data.chomp))
|
|
76
|
+
end
|
|
77
|
+
reset_event
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def reset_event
|
|
81
|
+
@id = nil
|
|
82
|
+
@type = nil
|
|
83
|
+
@data = "".b
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# UTF-8; malformed bytes become U+FFFD.
|
|
87
|
+
def text(bytes) = bytes.dup.force_encoding(Encoding::UTF_8).scrub
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module FlexiQ
|
|
6
|
+
module Wire
|
|
7
|
+
# proto3 JSON `google.protobuf.Timestamp`: an RFC 3339 instant in UTC.
|
|
8
|
+
module Timestamp
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def dump(time)
|
|
12
|
+
raise ArgumentError, "a timestamp must be a Time, got #{time.class}" unless time.is_a?(Time)
|
|
13
|
+
|
|
14
|
+
time.getutc.iso8601(9)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# nil for an absent or unreadable value: a bad timestamp should not sink the whole job.
|
|
18
|
+
def load(text)
|
|
19
|
+
return nil if text.nil?
|
|
20
|
+
|
|
21
|
+
Time.iso8601(text)
|
|
22
|
+
rescue ArgumentError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# The dynamic workflow constructs. `Client#submit_workflow` refuses any node setting one, with
|
|
5
|
+
# reason WORKFLOW_CONSTRUCT_UNSUPPORTED: only a live SDK process can advance them. They are
|
|
6
|
+
# here because the wire carries them and the refusal names the field it refused.
|
|
7
|
+
#
|
|
8
|
+
# Durations are seconds; an unset field is omitted, and an empty construct still counts as set.
|
|
9
|
+
|
|
10
|
+
# Pause the node for an external decision. `on_timeout` is `:approve` or `:reject`.
|
|
11
|
+
GateConfig = Data.define(:timeout, :on_timeout, :message) do
|
|
12
|
+
def initialize(timeout: nil, on_timeout: nil, message: nil) = super
|
|
13
|
+
|
|
14
|
+
def to_wire
|
|
15
|
+
{
|
|
16
|
+
"timeout" => timeout && Wire::Duration.dump(timeout),
|
|
17
|
+
"onTimeout" => on_timeout && on_timeout_wire,
|
|
18
|
+
"message" => message
|
|
19
|
+
}.compact
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def on_timeout_wire
|
|
25
|
+
case on_timeout
|
|
26
|
+
when :approve then "ON_TIMEOUT_APPROVE"
|
|
27
|
+
when :reject then "ON_TIMEOUT_REJECT"
|
|
28
|
+
else raise ArgumentError, "a gate's on_timeout must be :approve or :reject, got #{on_timeout.inspect}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Memoize the node's result; nil `ttl` caches indefinitely.
|
|
34
|
+
CacheConfig = Data.define(:ttl) do
|
|
35
|
+
def initialize(ttl: nil) = super
|
|
36
|
+
|
|
37
|
+
def to_wire = { "ttl" => ttl && Wire::Duration.dump(ttl) }.compact
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Expand the node into one job per item of `items_from`'s result.
|
|
41
|
+
FanOutConfig = Data.define(:items_from) do
|
|
42
|
+
def initialize(items_from: nil) = super
|
|
43
|
+
|
|
44
|
+
def to_wire = { "itemsFrom" => items_from }.compact
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Collect the fan-out node `from` back into one node.
|
|
48
|
+
FanInConfig = Data.define(:from) do
|
|
49
|
+
def to_wire = { "from" => from }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run a child workflow as this node.
|
|
53
|
+
SubWorkflowSpec = Data.define(:name, :version, :graph, :deferred_node_names) do
|
|
54
|
+
def initialize(name:, graph:, version: nil, deferred_node_names: [])
|
|
55
|
+
graph = WorkflowGraph.new(**graph) if graph.is_a?(Hash)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_wire
|
|
60
|
+
{
|
|
61
|
+
"name" => name, "version" => version, "graph" => graph.to_wire,
|
|
62
|
+
"deferredNodeNames" => deferred_node_names.empty? ? nil : deferred_node_names
|
|
63
|
+
}.compact
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# One edge of a workflow graph: `from` must finish before `to` becomes eligible.
|
|
5
|
+
WorkflowEdge = Data.define(:from, :to) do
|
|
6
|
+
# An edge from a WorkflowEdge, a `[from, to]` pair or a Hash of its fields.
|
|
7
|
+
def self.coerce(edge)
|
|
8
|
+
case edge
|
|
9
|
+
when WorkflowEdge then edge
|
|
10
|
+
when Hash then new(**edge)
|
|
11
|
+
when Array
|
|
12
|
+
raise ArgumentError, "an edge pair must have two names, got #{edge.length}" unless edge.length == 2
|
|
13
|
+
|
|
14
|
+
new(from: edge[0], to: edge[1])
|
|
15
|
+
else raise ArgumentError, "an edge must be a WorkflowEdge, a [from, to] pair or a Hash, got #{edge.class}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_wire = { "from" => from, "to" => to }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# A workflow's shape: its nodes and the edges between them.
|
|
5
|
+
#
|
|
6
|
+
# `nodes` are WorkflowNode (or Hashes of its fields); `edges` are WorkflowEdge, `[from, to]`
|
|
7
|
+
# pairs or Hashes. A node with no inbound edge is eligible as soon as the run starts.
|
|
8
|
+
WorkflowGraph = Data.define(:nodes, :edges) do
|
|
9
|
+
def initialize(nodes:, edges: [])
|
|
10
|
+
raise ArgumentError, "nodes must be an Array, got #{nodes.class}" unless nodes.is_a?(Array)
|
|
11
|
+
raise ArgumentError, "edges must be an Array, got #{edges.class}" unless edges.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
nodes = nodes.map { |node| node.is_a?(Hash) ? WorkflowNode.new(**node) : node }
|
|
14
|
+
super(nodes: nodes.freeze, edges: edges.map { |edge| WorkflowEdge.coerce(edge) }.freeze)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Refuses, before any call, a graph the server would refuse in terms of its own compilation:
|
|
18
|
+
# an edge to an undeclared node fails there as "missing from step_metadata".
|
|
19
|
+
# A nested sub-workflow graph is not checked; the server refuses the node before reading it.
|
|
20
|
+
def validate!
|
|
21
|
+
raise ArgumentError, "a workflow graph needs at least one node" if nodes.empty?
|
|
22
|
+
|
|
23
|
+
declared = {}
|
|
24
|
+
nodes.each do |node|
|
|
25
|
+
raise ArgumentError, "a graph node must be a WorkflowNode, got #{node.class}" unless node.is_a?(WorkflowNode)
|
|
26
|
+
raise ArgumentError, "node #{node.name.inspect} is declared twice" if declared.key?(node.name)
|
|
27
|
+
|
|
28
|
+
declared[node.name] = true
|
|
29
|
+
end
|
|
30
|
+
edges.each do |edge|
|
|
31
|
+
[edge.from, edge.to].each do |end_name|
|
|
32
|
+
next if declared.key?(end_name)
|
|
33
|
+
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"edge #{edge.from.inspect} -> #{edge.to.inspect} names undeclared node #{end_name.inspect}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The proto3 JSON `WorkflowGraph`: the bare shape, then each node's configuration by name.
|
|
42
|
+
def to_wire
|
|
43
|
+
{
|
|
44
|
+
"nodes" => nodes.map { |node| { "name" => node.name } },
|
|
45
|
+
"edges" => edges.map(&:to_wire),
|
|
46
|
+
"nodeConfigs" => nodes.map(&:to_wire)
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# One step of a workflow: its name in the graph, the task it runs, the call and its settings.
|
|
5
|
+
#
|
|
6
|
+
# The arguments travel as the `raw` CBOR envelope, the same bytes `Client#enqueue` sends.
|
|
7
|
+
# `timeout` is seconds; `condition` is a FlexiQ::EdgeCondition symbol; `compensate` names the
|
|
8
|
+
# task a saga rollback runs. `gate`, `cache`, `fan_out`, `fan_in` and `sub_workflow` take their
|
|
9
|
+
# FlexiQ construct (or a Hash of its fields) and are refused by the server; see GateConfig.
|
|
10
|
+
WorkflowNode = Data.define(
|
|
11
|
+
:name, :task_name, :args, :kwargs, :queue, :max_retries, :timeout, :priority, :condition,
|
|
12
|
+
:compensate, :gate, :cache, :fan_out, :fan_in, :sub_workflow
|
|
13
|
+
) do
|
|
14
|
+
def initialize(name:, task_name:, args: [], kwargs: {}, queue: nil, max_retries: nil, timeout: nil,
|
|
15
|
+
priority: nil, condition: nil, compensate: nil, gate: nil, cache: nil, fan_out: nil,
|
|
16
|
+
fan_in: nil, sub_workflow: nil)
|
|
17
|
+
raise ArgumentError, "a node name must be a non-empty String" unless name.is_a?(String) && !name.empty?
|
|
18
|
+
unless task_name.is_a?(String) && !task_name.empty?
|
|
19
|
+
raise ArgumentError, "node #{name.inspect} needs a non-empty task_name"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
super(
|
|
23
|
+
name: name, task_name: task_name, args: args, kwargs: kwargs, queue: queue, max_retries: max_retries,
|
|
24
|
+
timeout: timeout, priority: priority, condition: condition, compensate: compensate,
|
|
25
|
+
gate: coerce(gate, GateConfig), cache: coerce(cache, CacheConfig), fan_out: coerce(fan_out, FanOutConfig),
|
|
26
|
+
fan_in: coerce(fan_in, FanInConfig), sub_workflow: coerce(sub_workflow, SubWorkflowSpec)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The proto3 JSON `WorkflowNodeConfig` object, unset fields omitted.
|
|
31
|
+
def to_wire
|
|
32
|
+
{
|
|
33
|
+
"name" => name,
|
|
34
|
+
"taskName" => task_name,
|
|
35
|
+
"queue" => queue,
|
|
36
|
+
"raw" => Wire::Bytes.dump(Payload.encode_call(args, kwargs)),
|
|
37
|
+
"maxRetries" => max_retries,
|
|
38
|
+
"timeout" => timeout && Wire::Duration.dump(timeout),
|
|
39
|
+
"priority" => priority,
|
|
40
|
+
"condition" => EdgeCondition.dump(condition),
|
|
41
|
+
"gate" => gate&.to_wire,
|
|
42
|
+
"cache" => cache&.to_wire,
|
|
43
|
+
"fanOut" => fan_out&.to_wire,
|
|
44
|
+
"fanIn" => fan_in&.to_wire,
|
|
45
|
+
"subWorkflow" => sub_workflow&.to_wire,
|
|
46
|
+
"compensate" => compensate
|
|
47
|
+
}.compact
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def coerce(value, type) = value.is_a?(Hash) ? type.new(**value) : value
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# One node's state within a run. `status` is a FlexiQ::WorkflowNodeStatus symbol, or the raw
|
|
5
|
+
# wire value. `job_id` is nil until the node's job exists; read the job with `Client#get_job`.
|
|
6
|
+
WorkflowNodeState = Data.define(:name, :status, :job_id, :started_at, :completed_at, :error) do
|
|
7
|
+
def self.from_json(json)
|
|
8
|
+
new(
|
|
9
|
+
name: json["name"], status: WorkflowNodeStatus.load(json["status"]), job_id: json["jobId"],
|
|
10
|
+
started_at: Wire::Timestamp.load(json["startedAt"]),
|
|
11
|
+
completed_at: Wire::Timestamp.load(json["completedAt"]), error: json["error"]
|
|
12
|
+
)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def terminal? = WorkflowNodeStatus.terminal?(status)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# `WorkflowNodeStatus` as symbols. An unknown value stays raw and is never terminal.
|
|
5
|
+
# A runnable node reads `:pending`: readiness is a predicate over the graph, not a status.
|
|
6
|
+
module WorkflowNodeStatus
|
|
7
|
+
BY_WIRE_NAME = {
|
|
8
|
+
"WORKFLOW_NODE_STATUS_PENDING" => :pending,
|
|
9
|
+
"WORKFLOW_NODE_STATUS_RUNNING" => :running,
|
|
10
|
+
"WORKFLOW_NODE_STATUS_COMPLETED" => :completed,
|
|
11
|
+
"WORKFLOW_NODE_STATUS_FAILED" => :failed,
|
|
12
|
+
"WORKFLOW_NODE_STATUS_SKIPPED" => :skipped,
|
|
13
|
+
"WORKFLOW_NODE_STATUS_WAITING_APPROVAL" => :waiting_approval,
|
|
14
|
+
"WORKFLOW_NODE_STATUS_CACHE_HIT" => :cache_hit,
|
|
15
|
+
"WORKFLOW_NODE_STATUS_COMPENSATING" => :compensating,
|
|
16
|
+
"WORKFLOW_NODE_STATUS_COMPENSATED" => :compensated,
|
|
17
|
+
"WORKFLOW_NODE_STATUS_COMPENSATION_FAILED" => :compensation_failed
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
KNOWN = BY_WIRE_NAME.values.freeze
|
|
21
|
+
TERMINAL = %i[completed failed skipped cache_hit compensated compensation_failed].freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def load(value) = BY_WIRE_NAME.fetch(value, value)
|
|
26
|
+
|
|
27
|
+
def known?(status) = KNOWN.include?(status)
|
|
28
|
+
|
|
29
|
+
def terminal?(status) = TERMINAL.include?(status)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# A workflow run as the server reports it. `state` is a FlexiQ::WorkflowState symbol, or the
|
|
5
|
+
# raw wire value when this build does not know it. Timestamps are `Time` (UTC) or nil.
|
|
6
|
+
# `parent_run_id` / `parent_node_name` are set only on a sub-workflow's run.
|
|
7
|
+
WorkflowRun = Data.define(
|
|
8
|
+
:id, :definition_id, :state, :created_at, :started_at, :completed_at, :error,
|
|
9
|
+
:parent_run_id, :parent_node_name
|
|
10
|
+
) do
|
|
11
|
+
def self.from_json(json)
|
|
12
|
+
new(
|
|
13
|
+
id: json["id"], definition_id: json["definitionId"], state: WorkflowState.load(json["state"]),
|
|
14
|
+
created_at: Wire::Timestamp.load(json["createdAt"]),
|
|
15
|
+
started_at: Wire::Timestamp.load(json["startedAt"]),
|
|
16
|
+
completed_at: Wire::Timestamp.load(json["completedAt"]),
|
|
17
|
+
error: json["error"], parent_run_id: json["parentRunId"], parent_node_name: json["parentNodeName"]
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Finished for good. An unrecognised state is never terminal.
|
|
22
|
+
def terminal? = WorkflowState.terminal?(state)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# What `Client#get_workflow_run` returns: the run and every node it has, in no set order.
|
|
5
|
+
WorkflowRunView = Data.define(:run, :nodes) do
|
|
6
|
+
def self.from_json(json)
|
|
7
|
+
run = json["run"]
|
|
8
|
+
raise TransportError, "the server answered without a workflow run" unless run.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
nodes = json.fetch("nodes", [])
|
|
11
|
+
raise TransportError, "the server answered a run whose nodes are not a list" unless nodes.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
new(run: WorkflowRun.from_json(run), nodes: nodes.map { |node| WorkflowNodeState.from_json(node) }.freeze)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The node named `name`, or nil.
|
|
17
|
+
def node(name) = nodes.find { |node| node.name == name }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FlexiQ
|
|
4
|
+
# `WorkflowState` as symbols. An unknown value stays raw and is never terminal: reading a
|
|
5
|
+
# newer server's state as finished would stop a poller watching a run that is still going.
|
|
6
|
+
module WorkflowState
|
|
7
|
+
BY_WIRE_NAME = {
|
|
8
|
+
"WORKFLOW_STATE_PENDING" => :pending,
|
|
9
|
+
"WORKFLOW_STATE_RUNNING" => :running,
|
|
10
|
+
"WORKFLOW_STATE_PAUSED" => :paused,
|
|
11
|
+
"WORKFLOW_STATE_COMPLETED" => :completed,
|
|
12
|
+
"WORKFLOW_STATE_COMPLETED_WITH_FAILURES" => :completed_with_failures,
|
|
13
|
+
"WORKFLOW_STATE_FAILED" => :failed,
|
|
14
|
+
"WORKFLOW_STATE_CANCELLED" => :cancelled,
|
|
15
|
+
"WORKFLOW_STATE_COMPENSATING" => :compensating,
|
|
16
|
+
"WORKFLOW_STATE_COMPENSATED" => :compensated,
|
|
17
|
+
"WORKFLOW_STATE_COMPENSATION_FAILED" => :compensation_failed
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
KNOWN = BY_WIRE_NAME.values.freeze
|
|
21
|
+
TERMINAL = %i[completed completed_with_failures failed cancelled compensated compensation_failed].freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def load(value) = BY_WIRE_NAME.fetch(value, value)
|
|
26
|
+
|
|
27
|
+
def known?(state) = KNOWN.include?(state)
|
|
28
|
+
|
|
29
|
+
def terminal?(state) = TERMINAL.include?(state)
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/flexiq.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# FlexiQ producer client over a flexiq-server's JSON door.
|
|
4
|
+
# The wire contract is contracts/REMOTE_SDK_CONTRACT.md in the FlexiQ repository.
|
|
5
|
+
|
|
6
|
+
require_relative "flexiq/version"
|
|
7
|
+
require_relative "flexiq/errors"
|
|
8
|
+
require_relative "flexiq/deadline"
|
|
9
|
+
|
|
10
|
+
require_relative "flexiq/cbor/encoder"
|
|
11
|
+
require_relative "flexiq/cbor/decoder"
|
|
12
|
+
require_relative "flexiq/payload"
|
|
13
|
+
|
|
14
|
+
require_relative "flexiq/wire/bytes"
|
|
15
|
+
require_relative "flexiq/wire/duration"
|
|
16
|
+
require_relative "flexiq/wire/int64"
|
|
17
|
+
require_relative "flexiq/wire/path"
|
|
18
|
+
require_relative "flexiq/wire/sse"
|
|
19
|
+
require_relative "flexiq/wire/timestamp"
|
|
20
|
+
|
|
21
|
+
require_relative "flexiq/reason"
|
|
22
|
+
require_relative "flexiq/rpc_error"
|
|
23
|
+
|
|
24
|
+
require_relative "flexiq/job_status"
|
|
25
|
+
require_relative "flexiq/task_error"
|
|
26
|
+
require_relative "flexiq/job"
|
|
27
|
+
require_relative "flexiq/job_page"
|
|
28
|
+
require_relative "flexiq/debounce"
|
|
29
|
+
require_relative "flexiq/enqueue_options"
|
|
30
|
+
require_relative "flexiq/enqueue_request"
|
|
31
|
+
require_relative "flexiq/enqueue_result"
|
|
32
|
+
require_relative "flexiq/batch_item_result"
|
|
33
|
+
require_relative "flexiq/queue_stats"
|
|
34
|
+
require_relative "flexiq/transition_kind"
|
|
35
|
+
require_relative "flexiq/job_transition"
|
|
36
|
+
require_relative "flexiq/watch_items"
|
|
37
|
+
require_relative "flexiq/edge_condition"
|
|
38
|
+
require_relative "flexiq/workflow_edge"
|
|
39
|
+
require_relative "flexiq/workflow_constructs"
|
|
40
|
+
require_relative "flexiq/workflow_node"
|
|
41
|
+
require_relative "flexiq/workflow_graph"
|
|
42
|
+
require_relative "flexiq/workflow_state"
|
|
43
|
+
require_relative "flexiq/workflow_node_status"
|
|
44
|
+
require_relative "flexiq/workflow_run"
|
|
45
|
+
require_relative "flexiq/workflow_node_state"
|
|
46
|
+
require_relative "flexiq/workflow_run_view"
|
|
47
|
+
|
|
48
|
+
require_relative "flexiq/backoff"
|
|
49
|
+
require_relative "flexiq/transport"
|
|
50
|
+
require_relative "flexiq/watch"
|
|
51
|
+
require_relative "flexiq/watch/by_ids"
|
|
52
|
+
require_relative "flexiq/watch/by_queue"
|
|
53
|
+
require_relative "flexiq/client"
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: flexiq
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 2.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ByteVeda
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Submit, read, cancel and count FlexiQ jobs over a running flexiq-server's
|
|
14
|
+
JSON door. Pure Ruby, standard library only: no database credential, no
|
|
15
|
+
native extension, no gRPC toolchain.
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/flexiq.rb
|
|
23
|
+
- lib/flexiq/backoff.rb
|
|
24
|
+
- lib/flexiq/batch_item_result.rb
|
|
25
|
+
- lib/flexiq/cbor/decoder.rb
|
|
26
|
+
- lib/flexiq/cbor/encoder.rb
|
|
27
|
+
- lib/flexiq/client.rb
|
|
28
|
+
- lib/flexiq/deadline.rb
|
|
29
|
+
- lib/flexiq/debounce.rb
|
|
30
|
+
- lib/flexiq/edge_condition.rb
|
|
31
|
+
- lib/flexiq/enqueue_options.rb
|
|
32
|
+
- lib/flexiq/enqueue_request.rb
|
|
33
|
+
- lib/flexiq/enqueue_result.rb
|
|
34
|
+
- lib/flexiq/errors.rb
|
|
35
|
+
- lib/flexiq/job.rb
|
|
36
|
+
- lib/flexiq/job_page.rb
|
|
37
|
+
- lib/flexiq/job_status.rb
|
|
38
|
+
- lib/flexiq/job_transition.rb
|
|
39
|
+
- lib/flexiq/payload.rb
|
|
40
|
+
- lib/flexiq/queue_stats.rb
|
|
41
|
+
- lib/flexiq/reason.rb
|
|
42
|
+
- lib/flexiq/rpc_error.rb
|
|
43
|
+
- lib/flexiq/task_error.rb
|
|
44
|
+
- lib/flexiq/transition_kind.rb
|
|
45
|
+
- lib/flexiq/transport.rb
|
|
46
|
+
- lib/flexiq/version.rb
|
|
47
|
+
- lib/flexiq/watch.rb
|
|
48
|
+
- lib/flexiq/watch/by_ids.rb
|
|
49
|
+
- lib/flexiq/watch/by_queue.rb
|
|
50
|
+
- lib/flexiq/watch_items.rb
|
|
51
|
+
- lib/flexiq/wire/bytes.rb
|
|
52
|
+
- lib/flexiq/wire/duration.rb
|
|
53
|
+
- lib/flexiq/wire/int64.rb
|
|
54
|
+
- lib/flexiq/wire/path.rb
|
|
55
|
+
- lib/flexiq/wire/sse.rb
|
|
56
|
+
- lib/flexiq/wire/timestamp.rb
|
|
57
|
+
- lib/flexiq/workflow_constructs.rb
|
|
58
|
+
- lib/flexiq/workflow_edge.rb
|
|
59
|
+
- lib/flexiq/workflow_graph.rb
|
|
60
|
+
- lib/flexiq/workflow_node.rb
|
|
61
|
+
- lib/flexiq/workflow_node_state.rb
|
|
62
|
+
- lib/flexiq/workflow_node_status.rb
|
|
63
|
+
- lib/flexiq/workflow_run.rb
|
|
64
|
+
- lib/flexiq/workflow_run_view.rb
|
|
65
|
+
- lib/flexiq/workflow_state.rb
|
|
66
|
+
homepage: https://github.com/ByteVeda/flexiq
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
homepage_uri: https://github.com/ByteVeda/flexiq
|
|
71
|
+
source_code_uri: https://github.com/ByteVeda/flexiq/tree/master/sdks/ruby
|
|
72
|
+
documentation_uri: https://docs.byteveda.org/flexiq/server/clients
|
|
73
|
+
bug_tracker_uri: https://github.com/ByteVeda/flexiq/issues
|
|
74
|
+
rubygems_mfa_required: 'true'
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '3.3'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.6.9
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Ruby client for the FlexiQ producer door
|
|
92
|
+
test_files: []
|