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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +302 -0
  4. data/lib/flexiq/backoff.rb +21 -0
  5. data/lib/flexiq/batch_item_result.rb +21 -0
  6. data/lib/flexiq/cbor/decoder.rb +169 -0
  7. data/lib/flexiq/cbor/encoder.rb +112 -0
  8. data/lib/flexiq/client.rb +196 -0
  9. data/lib/flexiq/deadline.rb +24 -0
  10. data/lib/flexiq/debounce.rb +20 -0
  11. data/lib/flexiq/edge_condition.rb +24 -0
  12. data/lib/flexiq/enqueue_options.rb +38 -0
  13. data/lib/flexiq/enqueue_request.rb +25 -0
  14. data/lib/flexiq/enqueue_result.rb +13 -0
  15. data/lib/flexiq/errors.rb +21 -0
  16. data/lib/flexiq/job.rb +60 -0
  17. data/lib/flexiq/job_page.rb +19 -0
  18. data/lib/flexiq/job_status.rb +41 -0
  19. data/lib/flexiq/job_transition.rb +25 -0
  20. data/lib/flexiq/payload.rb +55 -0
  21. data/lib/flexiq/queue_stats.rb +12 -0
  22. data/lib/flexiq/reason.rb +52 -0
  23. data/lib/flexiq/rpc_error.rb +117 -0
  24. data/lib/flexiq/task_error.rb +50 -0
  25. data/lib/flexiq/transition_kind.rb +27 -0
  26. data/lib/flexiq/transport.rb +216 -0
  27. data/lib/flexiq/version.rb +6 -0
  28. data/lib/flexiq/watch/by_ids.rb +33 -0
  29. data/lib/flexiq/watch/by_queue.rb +33 -0
  30. data/lib/flexiq/watch.rb +132 -0
  31. data/lib/flexiq/watch_items.rb +13 -0
  32. data/lib/flexiq/wire/bytes.rb +23 -0
  33. data/lib/flexiq/wire/duration.rb +41 -0
  34. data/lib/flexiq/wire/int64.rb +20 -0
  35. data/lib/flexiq/wire/path.rb +20 -0
  36. data/lib/flexiq/wire/sse.rb +91 -0
  37. data/lib/flexiq/wire/timestamp.rb +27 -0
  38. data/lib/flexiq/workflow_constructs.rb +66 -0
  39. data/lib/flexiq/workflow_edge.rb +21 -0
  40. data/lib/flexiq/workflow_graph.rb +50 -0
  41. data/lib/flexiq/workflow_node.rb +54 -0
  42. data/lib/flexiq/workflow_node_state.rb +17 -0
  43. data/lib/flexiq/workflow_node_status.rb +31 -0
  44. data/lib/flexiq/workflow_run.rb +24 -0
  45. data/lib/flexiq/workflow_run_view.rb +19 -0
  46. data/lib/flexiq/workflow_state.rb +31 -0
  47. data/lib/flexiq.rb +53 -0
  48. 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: []