streamdal 0.0.1 → 0.0.3
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 +4 -4
- data/lib/audiences.rb +5 -5
- data/lib/hostfunc.rb +78 -23
- data/lib/kv.rb +3 -1
- data/lib/metrics.rb +52 -65
- data/lib/schema.rb +8 -14
- data/lib/streamdal.rb +77 -161
- data/lib/tail.rb +22 -28
- data/lib/wasm_spec.rb +259 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff825b88dbb81240b3ea5c3fd2cf264a88ecdafbb197ba886ed11acfe098ab93
|
4
|
+
data.tar.gz: 11a9e57851f4296c715b393d8c3ca7f33506d0050e6a4bf74121f92c141288c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9a744de7a691f637c48289c4647a3b369e89659c02ba4140b57addd8dbf5c3b98e769016b8cdd9158563622570aa82e98bd36fba237f6ec8802590c6c63f943
|
7
|
+
data.tar.gz: 698cdc39714da510b577bd7bc98d8da174da6c13c32c32c47041e51c64d444e7d5368d3f090b3ec239092fa3db01c93248a7ef14512491c0d6e2122b2446d58c
|
data/lib/audiences.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Audiences
|
2
4
|
def aud_to_str(aud)
|
3
5
|
"#{aud.service_name}.#{aud.component_name}.#{aud.operation_type}.#{aud.operation_name}"
|
@@ -5,7 +7,7 @@ module Audiences
|
|
5
7
|
|
6
8
|
def str_to_aud(str)
|
7
9
|
# TODO: move to common package
|
8
|
-
parts = str.split(
|
10
|
+
parts = str.split('.')
|
9
11
|
aud = Streamdal::Protos::Audience.new
|
10
12
|
aud.service_name = parts[0]
|
11
13
|
aud.component_name = parts[1]
|
@@ -20,9 +22,7 @@ module Audiences
|
|
20
22
|
|
21
23
|
def _add_audience(aud)
|
22
24
|
# Add an audience to the local cache map and send to server
|
23
|
-
if _seen_audience(aud)
|
24
|
-
return
|
25
|
-
end
|
25
|
+
return if _seen_audience(aud)
|
26
26
|
|
27
27
|
@audiences[aud_to_str(aud)] = aud
|
28
28
|
|
@@ -42,4 +42,4 @@ module Audiences
|
|
42
42
|
@stub.new_audience(req, metadata: _metadata)
|
43
43
|
end
|
44
44
|
end
|
45
|
-
end
|
45
|
+
end
|
data/lib/hostfunc.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
|
-
require
|
1
|
+
require 'steps/sp_steps_kv_pb'
|
2
|
+
require 'sp_wsm_pb'
|
3
|
+
require 'steps/sp_steps_httprequest_pb'
|
2
4
|
|
3
5
|
module Streamdal
|
4
6
|
class HostFunc
|
@@ -14,7 +16,7 @@ module Streamdal
|
|
14
16
|
# kv_exists is a host function that is used to check if a key exists in the KV store
|
15
17
|
def kv_exists(caller, ptr, len)
|
16
18
|
|
17
|
-
data = caller.export(
|
19
|
+
data = caller.export('memory').to_memory.read(ptr, len)
|
18
20
|
|
19
21
|
# Read request from memory and decode into HttpRequest
|
20
22
|
req = Streamdal::Protos::KVStep.decode(data)
|
@@ -35,32 +37,47 @@ module Streamdal
|
|
35
37
|
##
|
36
38
|
# http_request performs a http request on behalf of a wasm module since WASI cannot talk sockets
|
37
39
|
def http_request(caller, ptr, len)
|
38
|
-
data = caller.export(
|
40
|
+
data = caller.export('memory').to_memory.read(ptr, len)
|
39
41
|
|
40
42
|
# Read request from memory and decode into HttpRequest
|
41
|
-
req = Streamdal::Protos::
|
43
|
+
req = Streamdal::Protos::WASMRequest.decode(data)
|
44
|
+
|
45
|
+
begin
|
46
|
+
req_body = _get_request_body_for_mode(req)
|
47
|
+
rescue => e
|
48
|
+
return _http_request_response(caller, 400, e.to_s, {})
|
49
|
+
end
|
42
50
|
|
43
51
|
# Attempt to make HTTP request
|
44
52
|
# On error, return a mock 400 response with the error as the body
|
45
53
|
begin
|
46
|
-
response = _make_http_request(req)
|
54
|
+
response = _make_http_request(req.step.http_request.request, req_body)
|
47
55
|
rescue => e
|
48
|
-
|
49
|
-
wasm_resp.code = 400
|
50
|
-
wasm_resp.body = "Unable to execute HTTP request: #{e}"
|
51
|
-
return wasm_resp
|
56
|
+
return _http_request_response(caller, 400, "Unable to execute HTTP request: #{e}", {})
|
52
57
|
end
|
53
58
|
|
54
|
-
#
|
59
|
+
# Convert body to utf8
|
60
|
+
out = encode(response.body)
|
61
|
+
|
62
|
+
_http_request_response(caller, response.code, out, response.headers)
|
63
|
+
end
|
64
|
+
|
65
|
+
def encode(str)
|
66
|
+
str.force_encoding('ascii-8bit').encode('utf-8', invalid: :replace, undef: :replace, replace: '?')
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def _http_request_response(caller, code, body, headers)
|
55
72
|
wasm_resp = Streamdal::Protos::HttpResponse.new
|
56
|
-
wasm_resp.code =
|
57
|
-
wasm_resp.body =
|
73
|
+
wasm_resp.code = code
|
74
|
+
wasm_resp.body = body
|
58
75
|
wasm_resp.headers = Google::Protobuf::Map.new(:string, :string, {})
|
59
76
|
|
60
77
|
# Headers can have multiple values, but we just want a map[string]string here for simplicity
|
61
78
|
# The client can pase by the delimiter ";" if needed.
|
62
|
-
|
63
|
-
wasm_resp.headers[k] = values.
|
79
|
+
headers.each do |k, values|
|
80
|
+
wasm_resp.headers[k] = values.is_a?(Array) ? values.join('; ') : values
|
64
81
|
end
|
65
82
|
|
66
83
|
# Write the HttpResponse proto message to WASM memory
|
@@ -68,17 +85,35 @@ module Streamdal
|
|
68
85
|
write_to_memory(caller, wasm_resp)
|
69
86
|
end
|
70
87
|
|
71
|
-
|
88
|
+
def _get_request_body_for_mode(req)
|
89
|
+
http_req = req.step.http_request.request
|
90
|
+
|
91
|
+
case http_req.body_mode
|
92
|
+
when :HTTP_REQUEST_BODY_MODE_INTER_STEP_RESULT
|
93
|
+
raise 'Inter step result is empty' if req.inter_step_result.nil?
|
94
|
+
|
95
|
+
detective_res = req.inter_step_result.detective_result
|
96
|
+
|
97
|
+
raise 'Detective result is empty' if detective_res.nil?
|
98
|
+
|
99
|
+
# Wipe values to prevent PII from being leaked
|
100
|
+
detective_res.matches.each { |step_res|
|
101
|
+
step_res.value = ''
|
102
|
+
}
|
103
|
+
|
104
|
+
req.inter_step_result.to_json
|
105
|
+
else
|
106
|
+
http_req.body
|
107
|
+
end
|
108
|
+
end
|
72
109
|
|
73
110
|
##
|
74
111
|
# Performs an http request
|
75
|
-
def _make_http_request(req)
|
76
|
-
if req.nil?
|
77
|
-
raise "req is required"
|
78
|
-
end
|
112
|
+
def _make_http_request(req, body)
|
113
|
+
raise 'req is required' if req.nil?
|
79
114
|
|
80
115
|
options = {
|
81
|
-
headers: { "Content-Type":
|
116
|
+
headers: { "Content-Type": 'application/json' }
|
82
117
|
}
|
83
118
|
|
84
119
|
req.headers.each { |key, value| options.headers[key] = value }
|
@@ -87,15 +122,15 @@ module Streamdal
|
|
87
122
|
when :HTTP_REQUEST_METHOD_GET
|
88
123
|
return HTTParty.get(req.url)
|
89
124
|
when :HTTP_REQUEST_METHOD_POST
|
90
|
-
options.body =
|
125
|
+
options.body = body
|
91
126
|
return HTTParty.post(req.url, options)
|
92
127
|
when :HTTP_REQUEST_METHOD_PUT
|
93
|
-
options.body =
|
128
|
+
options.body = body
|
94
129
|
return HTTParty.put(req.url, options)
|
95
130
|
when :HTTP_REQUEST_METHOD_DELETE
|
96
131
|
return HTTParty.delete(req.url)
|
97
132
|
when :HTTP_REQUEST_METHOD_PATCH
|
98
|
-
options.body =
|
133
|
+
options.body = body
|
99
134
|
return HTTParty.patch(req.url, options)
|
100
135
|
when :HTTP_REQUEST_METHOD_HEAD
|
101
136
|
return HTTParty.head(req.url)
|
@@ -105,5 +140,25 @@ module Streamdal
|
|
105
140
|
raise ArgumentError, "Invalid http request method: #{req.method}"
|
106
141
|
end
|
107
142
|
end
|
143
|
+
|
144
|
+
# Called by host functions to write memory to wasm instance so that
|
145
|
+
# the wasm module can read the result of a host function call
|
146
|
+
def write_to_memory(caller, res)
|
147
|
+
alloc = caller.export('alloc').to_func
|
148
|
+
memory = caller.export('memory').to_memory
|
149
|
+
|
150
|
+
# Serialize protobuf message
|
151
|
+
resp = res.to_proto
|
152
|
+
|
153
|
+
# Allocate memory for response
|
154
|
+
resp_ptr = alloc.call(resp.length)
|
155
|
+
|
156
|
+
# Write response to memory
|
157
|
+
memory.write(resp_ptr, resp)
|
158
|
+
|
159
|
+
# return 64bit integer where first 32 bits is the pointer, and the last 32 is the length
|
160
|
+
resp_ptr << 32 | resp.length
|
161
|
+
end
|
162
|
+
|
108
163
|
end
|
109
164
|
end
|
data/lib/kv.rb
CHANGED
data/lib/metrics.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Streamdal
|
2
4
|
class Counter
|
3
5
|
attr_accessor :last_updated, :name, :aud, :labels
|
@@ -6,15 +8,15 @@ module Streamdal
|
|
6
8
|
@name = name
|
7
9
|
@aud = aud
|
8
10
|
@labels = labels
|
9
|
-
@value =
|
10
|
-
@last_updated = Time
|
11
|
+
@value = value
|
12
|
+
@last_updated = Time.now
|
11
13
|
@value_mtx = Mutex.new
|
12
14
|
end
|
13
15
|
|
14
16
|
def incr(val)
|
15
17
|
@value_mtx.synchronize do
|
16
|
-
@value
|
17
|
-
@last_updated = Time
|
18
|
+
@value += val
|
19
|
+
@last_updated = Time.now
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
@@ -33,18 +35,18 @@ module Streamdal
|
|
33
35
|
|
34
36
|
class Metrics
|
35
37
|
|
36
|
-
COUNTER_CONSUME_BYTES =
|
37
|
-
COUNTER_CONSUME_PROCESSED =
|
38
|
-
COUNTER_CONSUME_ERRORS =
|
39
|
-
COUNTER_PRODUCE_BYTES =
|
40
|
-
COUNTER_PRODUCE_PROCESSED =
|
41
|
-
COUNTER_PRODUCE_ERRORS =
|
42
|
-
COUNTER_NOTIFY =
|
43
|
-
COUNTER_DROPPED_TAIL_MESSAGES =
|
44
|
-
COUNTER_CONSUME_BYTES_RATE =
|
45
|
-
COUNTER_PRODUCE_BYTES_RATE =
|
46
|
-
COUNTER_CONSUME_PROCESSED_RATE =
|
47
|
-
COUNTER_PRODUCE_PROCESSED_RATE =
|
38
|
+
COUNTER_CONSUME_BYTES = 'counter_consume_bytes'
|
39
|
+
COUNTER_CONSUME_PROCESSED = 'counter_consume_processed'
|
40
|
+
COUNTER_CONSUME_ERRORS = 'counter_consume_errors'
|
41
|
+
COUNTER_PRODUCE_BYTES = 'counter_produce_bytes'
|
42
|
+
COUNTER_PRODUCE_PROCESSED = 'counter_produce_processed'
|
43
|
+
COUNTER_PRODUCE_ERRORS = 'counter_produce_errors'
|
44
|
+
COUNTER_NOTIFY = 'counter_notify'
|
45
|
+
COUNTER_DROPPED_TAIL_MESSAGES = 'counter_dropped_tail_messages'
|
46
|
+
COUNTER_CONSUME_BYTES_RATE = 'counter_consume_bytes_rate'
|
47
|
+
COUNTER_PRODUCE_BYTES_RATE = 'counter_produce_bytes_rate'
|
48
|
+
COUNTER_CONSUME_PROCESSED_RATE = 'counter_consume_processed_rate'
|
49
|
+
COUNTER_PRODUCE_PROCESSED_RATE = 'counter_produce_processed_rate'
|
48
50
|
|
49
51
|
WORKER_POOL_SIZE = 3
|
50
52
|
DEFAULT_COUNTER_REAPER_INTERVAL = 10
|
@@ -54,9 +56,7 @@ module Streamdal
|
|
54
56
|
CounterEntry = Struct.new(:name, :aud, :labels, :value)
|
55
57
|
|
56
58
|
def initialize(cfg)
|
57
|
-
if cfg.nil?
|
58
|
-
raise ArgumentError, "cfg is nil"
|
59
|
-
end
|
59
|
+
raise ArgumentError, 'cfg is nil' if cfg.nil?
|
60
60
|
|
61
61
|
@cfg = cfg
|
62
62
|
@log = cfg[:log]
|
@@ -80,30 +80,22 @@ module Streamdal
|
|
80
80
|
|
81
81
|
# Exit any remaining threads
|
82
82
|
@workers.each do |w|
|
83
|
-
if w.running?
|
84
|
-
w.exit
|
85
|
-
end
|
83
|
+
w.exit if w.running?
|
86
84
|
end
|
87
85
|
end
|
88
86
|
|
89
87
|
def self.composite_id(counter_name, labels = {})
|
90
|
-
if labels.nil?
|
91
|
-
|
92
|
-
end
|
93
|
-
"#{counter_name}-#{labels.values.join("-")}".freeze
|
88
|
+
labels = {} if labels.nil?
|
89
|
+
"#{counter_name}-#{labels.values.join('-')}"
|
94
90
|
end
|
95
91
|
|
96
92
|
def get_counter(ce)
|
97
|
-
if ce.nil?
|
98
|
-
raise ArgumentError, "ce is nil"
|
99
|
-
end
|
93
|
+
raise ArgumentError, 'ce is nil' if ce.nil?
|
100
94
|
|
101
|
-
k = Metrics
|
95
|
+
k = Metrics.composite_id(ce.name, ce.labels)
|
102
96
|
|
103
97
|
@counters_mtx.synchronize do
|
104
|
-
if @counters.key?(k)
|
105
|
-
@counters[k]
|
106
|
-
end
|
98
|
+
@counters[k] if @counters.key?(k)
|
107
99
|
end
|
108
100
|
|
109
101
|
# No counter exists, create a new one and return it
|
@@ -114,7 +106,7 @@ module Streamdal
|
|
114
106
|
c = Counter.new(ce.name, ce.aud, ce.labels, ce.value)
|
115
107
|
|
116
108
|
@counters_mtx.synchronize do
|
117
|
-
@counters[Metrics
|
109
|
+
@counters[Metrics.composite_id(ce.name, ce.labels)] = c
|
118
110
|
end
|
119
111
|
|
120
112
|
c
|
@@ -166,31 +158,29 @@ module Streamdal
|
|
166
158
|
def _run_publisher
|
167
159
|
# Background thread that reads values from counters, adds them to the publish queue, and then
|
168
160
|
# resets the counter's value back to zero
|
169
|
-
|
170
|
-
@log.debug("Starting publisher")
|
161
|
+
return if @exit
|
171
162
|
|
172
|
-
|
173
|
-
sleep(DEFAULT_COUNTER_PUBLISH_INTERVAL)
|
163
|
+
@log.debug('Starting publisher')
|
174
164
|
|
175
|
-
|
176
|
-
|
177
|
-
# if value > 0, continue
|
178
|
-
# if now() - last_updated > 10 seconds, remove counter
|
179
|
-
# Grab copy of counters
|
180
|
-
@counters_mtx.lock
|
181
|
-
new_counters = @counters.dup
|
182
|
-
@counters_mtx.unlock
|
165
|
+
# Sleep on startup and then and between each loop run
|
166
|
+
sleep(DEFAULT_COUNTER_PUBLISH_INTERVAL)
|
183
167
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
168
|
+
# Get all counters
|
169
|
+
# Loop over each counter, get the value,
|
170
|
+
# if value > 0, continue
|
171
|
+
# if now() - last_updated > 10 seconds, remove counter
|
172
|
+
# Grab copy of counters
|
173
|
+
@counters_mtx.lock
|
174
|
+
new_counters = @counters.dup
|
175
|
+
@counters_mtx.unlock
|
188
176
|
|
189
|
-
|
190
|
-
|
177
|
+
new_counters.each_value do |counter|
|
178
|
+
next if counter.val.zero?
|
191
179
|
|
192
|
-
|
193
|
-
|
180
|
+
ce = CounterEntry.new(counter.name, counter.aud, counter.labels, counter.val)
|
181
|
+
counter.reset
|
182
|
+
|
183
|
+
@publish_queue.push(ce)
|
194
184
|
end
|
195
185
|
end
|
196
186
|
|
@@ -199,9 +189,8 @@ module Streamdal
|
|
199
189
|
|
200
190
|
until @exit
|
201
191
|
ce = @incr_queue.pop
|
202
|
-
if ce.nil?
|
203
|
-
|
204
|
-
end
|
192
|
+
next if ce.nil?
|
193
|
+
|
205
194
|
begin
|
206
195
|
_publish_metrics(ce)
|
207
196
|
rescue => e
|
@@ -213,7 +202,7 @@ module Streamdal
|
|
213
202
|
end
|
214
203
|
|
215
204
|
def _run_reaper
|
216
|
-
@log.debug(
|
205
|
+
@log.debug('Starting reaper')
|
217
206
|
|
218
207
|
until @exit
|
219
208
|
# Sleep on startup and then and between each loop run
|
@@ -226,11 +215,9 @@ module Streamdal
|
|
226
215
|
# Grab copy of counters
|
227
216
|
@counters_mtx.synchronize do
|
228
217
|
@counters.each do |name, counter|
|
229
|
-
if counter.val
|
230
|
-
next
|
231
|
-
end
|
218
|
+
next if counter.val.positive?
|
232
219
|
|
233
|
-
if Time
|
220
|
+
if Time.now - counter.last_updated > DEFAULT_COUNTER_TTL
|
234
221
|
@log.debug("Reaping counter '#{name}'")
|
235
222
|
@counters.delete(name)
|
236
223
|
end
|
@@ -238,7 +225,7 @@ module Streamdal
|
|
238
225
|
end
|
239
226
|
end
|
240
227
|
|
241
|
-
@log.debug(
|
228
|
+
@log.debug('Exiting reaper')
|
242
229
|
end
|
243
230
|
|
244
231
|
def _run_incrementer_worker(worker_id)
|
@@ -259,7 +246,7 @@ module Streamdal
|
|
259
246
|
|
260
247
|
# Returns metadata for gRPC requests to the internal gRPC API
|
261
248
|
def _metadata
|
262
|
-
{
|
249
|
+
{ 'auth-token' => @cfg[:streamdal_token].to_s }
|
263
250
|
end
|
264
251
|
end
|
265
|
-
end
|
252
|
+
end
|
data/lib/schema.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
include Streamdal::Protos
|
2
4
|
|
3
5
|
module Schemas
|
@@ -8,29 +10,21 @@ module Schemas
|
|
8
10
|
end
|
9
11
|
|
10
12
|
def _get_schema(aud)
|
11
|
-
if @schemas.key?(aud_to_str(aud))
|
12
|
-
return @schemas[aud_to_str(aud)].json_schema
|
13
|
-
end
|
13
|
+
return @schemas[aud_to_str(aud)].json_schema if @schemas.key?(aud_to_str(aud))
|
14
14
|
|
15
|
-
|
15
|
+
''
|
16
16
|
end
|
17
17
|
|
18
18
|
def _handle_schema(aud, step, wasm_resp)
|
19
19
|
# Only handle schema steps
|
20
|
-
if step.infer_schema.nil?
|
21
|
-
return nil
|
22
|
-
end
|
20
|
+
return nil if step.infer_schema.nil?
|
23
21
|
|
24
22
|
# Only successful schema inferences
|
25
|
-
if wasm_resp.exit_code != :WASM_EXIT_CODE_TRUE
|
26
|
-
return nil
|
27
|
-
end
|
23
|
+
return nil if wasm_resp.exit_code != :WASM_EXIT_CODE_TRUE
|
28
24
|
|
29
25
|
# If existing schema matches, do nothing
|
30
26
|
existing_schema = _get_schema(aud)
|
31
|
-
if existing_schema == wasm_resp.output_step
|
32
|
-
return nil
|
33
|
-
end
|
27
|
+
return nil if existing_schema == wasm_resp.output_step
|
34
28
|
|
35
29
|
_set_schema(aud, wasm_resp.output_step)
|
36
30
|
|
@@ -44,4 +38,4 @@ module Schemas
|
|
44
38
|
@stub.send_schema(req, metadata: _metadata)
|
45
39
|
end
|
46
40
|
end
|
47
|
-
end
|
41
|
+
end
|
data/lib/streamdal.rb
CHANGED
@@ -8,11 +8,11 @@ require 'sp_sdk_pb'
|
|
8
8
|
require 'sp_common_pb'
|
9
9
|
require 'sp_info_pb'
|
10
10
|
require 'sp_internal_pb'
|
11
|
-
require
|
11
|
+
require 'sp_internal_services_pb'
|
12
12
|
require 'sp_pipeline_pb'
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
13
|
+
require 'sp_wsm_pb'
|
14
|
+
require 'steps/sp_steps_httprequest_pb'
|
15
|
+
require 'steps/sp_steps_kv_pb'
|
16
16
|
require 'timeout'
|
17
17
|
require 'google/protobuf'
|
18
18
|
require_relative 'audiences'
|
@@ -31,7 +31,6 @@ DEFAULT_HEARTBEAT_INTERVAL = 1 # 1 second
|
|
31
31
|
MAX_PAYLOAD_SIZE = 1024 * 1024 # 1 megabyte
|
32
32
|
|
33
33
|
module Streamdal
|
34
|
-
|
35
34
|
OPERATION_TYPE_PRODUCER = 2
|
36
35
|
OPERATION_TYPE_CONSUMER = 1
|
37
36
|
CLIENT_TYPE_SDK = 1
|
@@ -39,7 +38,6 @@ module Streamdal
|
|
39
38
|
|
40
39
|
# Data class to hold instantiated wasm functions
|
41
40
|
class WasmFunction
|
42
|
-
|
43
41
|
##
|
44
42
|
# Instance of an initialized wasm module and associated memory store
|
45
43
|
|
@@ -66,7 +64,6 @@ module Streamdal
|
|
66
64
|
end
|
67
65
|
end
|
68
66
|
|
69
|
-
|
70
67
|
class Client
|
71
68
|
|
72
69
|
##
|
@@ -117,20 +114,14 @@ module Streamdal
|
|
117
114
|
|
118
115
|
# Exit any remaining threads
|
119
116
|
@workers.each do |w|
|
120
|
-
if w.running?
|
121
|
-
w.exit
|
122
|
-
end
|
117
|
+
w.exit if w.running?
|
123
118
|
end
|
124
119
|
end
|
125
120
|
|
126
121
|
def process(data, audience)
|
127
|
-
if data.
|
128
|
-
raise "data is required"
|
129
|
-
end
|
122
|
+
raise 'data is required' if data.empty?
|
130
123
|
|
131
|
-
if audience.nil?
|
132
|
-
raise "audience is required"
|
133
|
-
end
|
124
|
+
raise 'audience is required' if audience.nil?
|
134
125
|
|
135
126
|
resp = Streamdal::Protos::SDKResponse.new
|
136
127
|
resp.status = :EXEC_STATUS_TRUE
|
@@ -144,8 +135,8 @@ module Streamdal
|
|
144
135
|
"operation_type": aud.operation_type,
|
145
136
|
"operation": aud.operation_name,
|
146
137
|
"component": aud.component_name,
|
147
|
-
"pipeline_name":
|
148
|
-
"pipeline_id":
|
138
|
+
"pipeline_name": '',
|
139
|
+
"pipeline_id": '',
|
149
140
|
}
|
150
141
|
|
151
142
|
# TODO: metrics
|
@@ -166,7 +157,7 @@ module Streamdal
|
|
166
157
|
if payload_size > MAX_PAYLOAD_SIZE
|
167
158
|
# TODO: add metrics
|
168
159
|
resp.status = :EXEC_STATUS_ERROR
|
169
|
-
resp.error =
|
160
|
+
resp.error = 'payload size exceeds maximum allowed size'
|
170
161
|
resp
|
171
162
|
end
|
172
163
|
|
@@ -174,8 +165,8 @@ module Streamdal
|
|
174
165
|
original_data = data
|
175
166
|
|
176
167
|
pipelines = _get_pipelines(aud)
|
177
|
-
if pipelines.
|
178
|
-
_send_tail(aud,
|
168
|
+
if pipelines.empty?
|
169
|
+
_send_tail(aud, '', original_data, original_data)
|
179
170
|
return resp
|
180
171
|
end
|
181
172
|
|
@@ -215,13 +206,9 @@ module Streamdal
|
|
215
206
|
break
|
216
207
|
end
|
217
208
|
|
218
|
-
if @cfg[:dry_run]
|
219
|
-
@log.debug "Running step '#{step.name}' in dry-run mode"
|
220
|
-
end
|
209
|
+
@log.debug "Running step '#{step.name}' in dry-run mode" if @cfg[:dry_run]
|
221
210
|
|
222
|
-
if wasm_resp.output_payload.length
|
223
|
-
resp.data = wasm_resp.output_payload
|
224
|
-
end
|
211
|
+
resp.data = wasm_resp.output_payload if wasm_resp.output_payload.length.positive?
|
225
212
|
|
226
213
|
_handle_schema(aud, step, wasm_resp)
|
227
214
|
|
@@ -296,10 +283,10 @@ module Streamdal
|
|
296
283
|
end # pipelines.each
|
297
284
|
end # timeout
|
298
285
|
|
299
|
-
_send_tail(aud,
|
286
|
+
_send_tail(aud, '', original_data, resp.data)
|
300
287
|
|
301
288
|
if @cfg[:dry_run]
|
302
|
-
@log.debug
|
289
|
+
@log.debug 'Dry-run, setting response data to original data'
|
303
290
|
resp.data = original_data
|
304
291
|
end
|
305
292
|
|
@@ -309,42 +296,32 @@ module Streamdal
|
|
309
296
|
private
|
310
297
|
|
311
298
|
def _validate_cfg(cfg)
|
312
|
-
if cfg[:streamdal_url].nil? || cfg[:streamdal_url].empty?
|
313
|
-
raise "streamdal_url is required"
|
314
|
-
end
|
299
|
+
raise 'streamdal_url is required' if cfg[:streamdal_url].nil? || cfg[:streamdal_url].empty?
|
315
300
|
|
316
|
-
if cfg[:streamdal_token].nil? || cfg[:streamdal_token].empty?
|
317
|
-
raise "streamdal_token is required"
|
318
|
-
end
|
301
|
+
raise 'streamdal_token is required' if cfg[:streamdal_token].nil? || cfg[:streamdal_token].empty?
|
319
302
|
|
320
|
-
if cfg[:service_name].nil? || cfg[:streamdal_token].empty?
|
321
|
-
raise "service_name is required"
|
322
|
-
end
|
303
|
+
raise 'service_name is required' if cfg[:service_name].nil? || cfg[:streamdal_token].empty?
|
323
304
|
|
324
305
|
if cfg[:log].nil? || cfg[:streamdal_token].empty?
|
325
|
-
logger = Logger.new(
|
306
|
+
logger = Logger.new($stdout)
|
326
307
|
logger.level = Logger::ERROR
|
327
308
|
cfg[:log] = logger
|
328
309
|
end
|
329
310
|
|
330
|
-
if cfg[:pipeline_timeout].nil?
|
331
|
-
cfg[:pipeline_timeout] = DEFAULT_PIPELINE_TIMEOUT
|
332
|
-
end
|
311
|
+
cfg[:pipeline_timeout] = DEFAULT_PIPELINE_TIMEOUT if cfg[:pipeline_timeout].nil?
|
333
312
|
|
334
|
-
if cfg[:step_timeout].nil?
|
335
|
-
cfg[:step_timeout] = DEFAULT_STEP_TIMEOUT
|
336
|
-
end
|
313
|
+
cfg[:step_timeout] = DEFAULT_STEP_TIMEOUT if cfg[:step_timeout].nil?
|
337
314
|
end
|
338
315
|
|
339
316
|
def _handle_command(cmd)
|
340
317
|
case cmd.command.to_s
|
341
|
-
when
|
318
|
+
when 'kv'
|
342
319
|
_handle_kv(cmd)
|
343
|
-
when
|
320
|
+
when 'tail'
|
344
321
|
_handle_tail_request(cmd)
|
345
|
-
when
|
322
|
+
when 'set_pipelines'
|
346
323
|
_set_pipelines(cmd)
|
347
|
-
when
|
324
|
+
when 'keep_alive'
|
348
325
|
# Do nothing
|
349
326
|
else
|
350
327
|
@log.error "unknown command type #{cmd.command}"
|
@@ -378,13 +355,11 @@ module Streamdal
|
|
378
355
|
end
|
379
356
|
|
380
357
|
def _set_pipelines(cmd)
|
381
|
-
if cmd.nil?
|
382
|
-
raise "cmd is required"
|
383
|
-
end
|
358
|
+
raise 'cmd is required' if cmd.nil?
|
384
359
|
|
385
360
|
cmd.set_pipelines.pipelines.each_with_index { |p, pIdx|
|
386
361
|
p.steps.each_with_index { |step, idx|
|
387
|
-
if step._wasm_bytes ==
|
362
|
+
if step._wasm_bytes == ''
|
388
363
|
if cmd.set_pipelines.wasm_modules.has_key?(step._wasm_id)
|
389
364
|
step._wasm_bytes = cmd.set_pipelines.wasm_modules[step._wasm_id].bytes
|
390
365
|
cmd.set_pipelines.pipelines[pIdx].steps[idx] = step
|
@@ -416,19 +391,17 @@ module Streamdal
|
|
416
391
|
def _get_function(step)
|
417
392
|
# We cache functions so we can eliminate the wasm bytes from steps to save on memory
|
418
393
|
# And also to avoid re-initializing the same function multiple times
|
419
|
-
if @functions.key?(step._wasm_id)
|
420
|
-
return @functions[step._wasm_id]
|
421
|
-
end
|
394
|
+
return @functions[step._wasm_id] if @functions.key?(step._wasm_id)
|
422
395
|
|
423
396
|
engine = Wasmtime::Engine.new
|
424
397
|
mod = Wasmtime::Module.new(engine, step._wasm_bytes)
|
425
398
|
linker = Wasmtime::Linker.new(engine, wasi: true)
|
426
399
|
|
427
|
-
linker.func_new(
|
400
|
+
linker.func_new('env', 'httpRequest', %i[i32 i32], [:i64]) do |caller, ptr, len|
|
428
401
|
@hostfunc.http_request(caller, ptr, len)
|
429
402
|
end
|
430
403
|
|
431
|
-
linker.func_new(
|
404
|
+
linker.func_new('env', 'kvExists', %i[i32 i32], [:i64]) do |caller, ptr, len|
|
432
405
|
@hostfunc.kv_exists(caller, ptr, len)
|
433
406
|
end
|
434
407
|
|
@@ -442,8 +415,6 @@ module Streamdal
|
|
442
415
|
|
443
416
|
instance = linker.instantiate(store, mod)
|
444
417
|
|
445
|
-
# TODO: host funcs
|
446
|
-
|
447
418
|
# Store in cache
|
448
419
|
func = WasmFunction.new
|
449
420
|
func.instance = instance
|
@@ -454,17 +425,11 @@ module Streamdal
|
|
454
425
|
end
|
455
426
|
|
456
427
|
def _call_wasm(step, data, isr)
|
457
|
-
if step.nil?
|
458
|
-
raise "step is required"
|
459
|
-
end
|
428
|
+
raise 'step is required' if step.nil?
|
460
429
|
|
461
|
-
if data.nil?
|
462
|
-
raise "data is required"
|
463
|
-
end
|
430
|
+
raise 'data is required' if data.nil?
|
464
431
|
|
465
|
-
if isr.nil?
|
466
|
-
isr = Streamdal::Protos::InterStepResult.new
|
467
|
-
end
|
432
|
+
isr = Streamdal::Protos::InterStepResult.new if isr.nil?
|
468
433
|
|
469
434
|
req = Streamdal::Protos::WASMRequest.new
|
470
435
|
req.step = step.clone
|
@@ -480,8 +445,8 @@ module Streamdal
|
|
480
445
|
resp = Streamdal::Protos::WASMResponse.new
|
481
446
|
resp.exit_code = :WASM_EXIT_CODE_ERROR
|
482
447
|
resp.exit_msg = "Failed to execute WASM: #{e}"
|
483
|
-
resp.output_payload =
|
484
|
-
|
448
|
+
resp.output_payload = ''
|
449
|
+
resp
|
485
450
|
end
|
486
451
|
end
|
487
452
|
|
@@ -500,9 +465,9 @@ module Streamdal
|
|
500
465
|
|
501
466
|
ci = Streamdal::Protos::ClientInfo.new
|
502
467
|
ci.client_type = :CLIENT_TYPE_SDK
|
503
|
-
ci.library_name =
|
504
|
-
ci.library_version =
|
505
|
-
ci.language =
|
468
|
+
ci.library_name = 'ruby-sdk'
|
469
|
+
ci.library_version = '0.0.1'
|
470
|
+
ci.language = 'ruby'
|
506
471
|
ci.arch = arch
|
507
472
|
ci.os = os
|
508
473
|
|
@@ -511,23 +476,21 @@ module Streamdal
|
|
511
476
|
|
512
477
|
# Returns metadata for gRPC requests to the internal gRPC API
|
513
478
|
def _metadata
|
514
|
-
{
|
479
|
+
{ 'auth-token' => @cfg[:streamdal_token].to_s }
|
515
480
|
end
|
516
481
|
|
517
482
|
def _register
|
518
|
-
@log.info(
|
483
|
+
@log.info('register started')
|
519
484
|
|
520
485
|
# Register with Streamdal External gRPC API
|
521
486
|
resps = @stub.register(_gen_register_request, metadata: _metadata)
|
522
487
|
resps.each do |r|
|
523
|
-
if @exit
|
524
|
-
break
|
525
|
-
end
|
488
|
+
break if @exit
|
526
489
|
|
527
490
|
_handle_command(r)
|
528
491
|
end
|
529
492
|
|
530
|
-
@log.info(
|
493
|
+
@log.info('register exited')
|
531
494
|
end
|
532
495
|
|
533
496
|
def _exec_wasm(req)
|
@@ -535,14 +498,14 @@ module Streamdal
|
|
535
498
|
|
536
499
|
# Empty out _wasm_bytes, we don't need it anymore
|
537
500
|
# TODO: does this actually update the original object?
|
538
|
-
req.step._wasm_bytes =
|
501
|
+
req.step._wasm_bytes = ''
|
539
502
|
|
540
503
|
data = req.to_proto
|
541
504
|
|
542
|
-
memory = wasm_func.instance.export(
|
543
|
-
alloc = wasm_func.instance.export(
|
544
|
-
dealloc = wasm_func.instance.export(
|
545
|
-
f = wasm_func.instance.export(
|
505
|
+
memory = wasm_func.instance.export('memory').to_memory
|
506
|
+
alloc = wasm_func.instance.export('alloc').to_func
|
507
|
+
dealloc = wasm_func.instance.export('dealloc').to_func
|
508
|
+
f = wasm_func.instance.export('f').to_func
|
546
509
|
|
547
510
|
start_ptr = alloc.call(data.length)
|
548
511
|
|
@@ -568,9 +531,7 @@ module Streamdal
|
|
568
531
|
|
569
532
|
_add_audience(aud)
|
570
533
|
|
571
|
-
if @pipelines.key?(aud_str)
|
572
|
-
return @pipelines[aud_str]
|
573
|
-
end
|
534
|
+
return @pipelines[aud_str] if @pipelines.key?(aud_str)
|
574
535
|
|
575
536
|
[]
|
576
537
|
end
|
@@ -581,7 +542,7 @@ module Streamdal
|
|
581
542
|
req.session_id = @session_id
|
582
543
|
req.audiences = Google::Protobuf::RepeatedField.new(:message, Streamdal::Protos::Audience, [])
|
583
544
|
|
584
|
-
@audiences.
|
545
|
+
@audiences.each_value do |aud|
|
585
546
|
req.audiences.push(aud)
|
586
547
|
end
|
587
548
|
|
@@ -616,18 +577,14 @@ module Streamdal
|
|
616
577
|
|
617
578
|
def _get_active_tails_for_audience(aud)
|
618
579
|
aud_str = aud_to_str(aud)
|
619
|
-
if @tails.key?(aud_str)
|
620
|
-
return @tails[aud_str].values
|
621
|
-
end
|
580
|
+
return @tails[aud_str].values if @tails.key?(aud_str)
|
622
581
|
|
623
582
|
[]
|
624
583
|
end
|
625
584
|
|
626
585
|
def _send_tail(aud, pipeline_id, original_data, new_data)
|
627
586
|
tails = _get_active_tails_for_audience(aud)
|
628
|
-
if tails.
|
629
|
-
return nil
|
630
|
-
end
|
587
|
+
return nil if tails.empty?
|
631
588
|
|
632
589
|
tails.each do |tail|
|
633
590
|
req = Streamdal::Protos::TailResponse.new
|
@@ -644,19 +601,13 @@ module Streamdal
|
|
644
601
|
end
|
645
602
|
|
646
603
|
def _notify_condition(pipeline, step, aud, cond, data, cond_type)
|
647
|
-
if cond.nil?
|
648
|
-
return nil
|
649
|
-
end
|
604
|
+
return nil if cond.nil?
|
650
605
|
|
651
|
-
if cond.notification.nil?
|
652
|
-
return nil
|
653
|
-
end
|
606
|
+
return nil if cond.notification.nil?
|
654
607
|
|
655
|
-
@log.debug
|
608
|
+
@log.debug 'Notifying'
|
656
609
|
|
657
|
-
if @cfg[:dry_run]
|
658
|
-
return nil
|
659
|
-
end
|
610
|
+
return nil if @cfg[:dry_run]
|
660
611
|
|
661
612
|
@metrics.incr(CounterEntry.new(Metrics::COUNTER_NOTIFY, aud, {
|
662
613
|
"service": @cfg[:service_name],
|
@@ -706,15 +657,12 @@ module Streamdal
|
|
706
657
|
t.start_tail_workers
|
707
658
|
|
708
659
|
_set_active_tail(t)
|
709
|
-
|
710
660
|
end
|
711
661
|
|
712
662
|
def _set_active_tail(tail)
|
713
663
|
key = aud_to_str(tail.request.audience)
|
714
664
|
|
715
|
-
unless @tails.key?(key)
|
716
|
-
@tails[key] = {}
|
717
|
-
end
|
665
|
+
@tails[key] = {} unless @tails.key?(key)
|
718
666
|
|
719
667
|
@tails[key][tail.request.id] = tail
|
720
668
|
end
|
@@ -722,9 +670,7 @@ module Streamdal
|
|
722
670
|
def _set_paused_tail(tail)
|
723
671
|
key = aud_to_str(tail.request.aud)
|
724
672
|
|
725
|
-
unless @paused_tails.key?(key)
|
726
|
-
@paused_tails[key] = {}
|
727
|
-
end
|
673
|
+
@paused_tails[key] = {} unless @paused_tails.key?(key)
|
728
674
|
|
729
675
|
@paused_tails[key][tail.request.id] = tail
|
730
676
|
end
|
@@ -739,18 +685,14 @@ module Streamdal
|
|
739
685
|
# Remove from active tails
|
740
686
|
@tails[key].delete(cmd.tail.request.id)
|
741
687
|
|
742
|
-
if @tails[key].
|
743
|
-
@tails.delete(key)
|
744
|
-
end
|
688
|
+
@tails.delete(key) if @tails[key].empty?
|
745
689
|
end
|
746
690
|
|
747
|
-
|
748
|
-
@paused_tails[key].delete(cmd.tail.request.id)
|
691
|
+
return unless @paused_tails.key?(key) && @paused_tails[key].key?(cmd.tail.request.id)
|
749
692
|
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
end
|
693
|
+
@paused_tails[key].delete(cmd.tail.request.id)
|
694
|
+
|
695
|
+
@paused_tails.delete(key) if @paused_tails[key].empty?
|
754
696
|
end
|
755
697
|
|
756
698
|
def _stop_all_tails
|
@@ -766,9 +708,7 @@ module Streamdal
|
|
766
708
|
t.stop_tail
|
767
709
|
tails[aud].delete(tail.request.id)
|
768
710
|
|
769
|
-
if tails[aud].
|
770
|
-
tails.delete(aud)
|
771
|
-
end
|
711
|
+
tails.delete(aud) if tails[aud].empty?
|
772
712
|
end
|
773
713
|
end
|
774
714
|
end
|
@@ -799,54 +739,30 @@ module Streamdal
|
|
799
739
|
def _remove_active_tail(aud, tail_id)
|
800
740
|
key = aud_to_str(aud)
|
801
741
|
|
802
|
-
|
803
|
-
t = @tails[key][tail_id]
|
804
|
-
t.stop_tail
|
742
|
+
return unless @tails.key?(key) && @tails[key].key?(tail_id)
|
805
743
|
|
806
|
-
|
744
|
+
t = @tails[key][tail_id]
|
745
|
+
t.stop_tail
|
807
746
|
|
808
|
-
|
809
|
-
@tails.delete(key)
|
810
|
-
end
|
747
|
+
@tails[key].delete(tail_id)
|
811
748
|
|
812
|
-
|
813
|
-
|
749
|
+
@tails.delete(key) if @tails[key].empty?
|
750
|
+
|
751
|
+
t
|
814
752
|
end
|
815
753
|
|
816
754
|
def _remove_paused_tail(aud, tail_id)
|
817
755
|
key = aud_to_str(aud)
|
818
756
|
|
819
|
-
|
820
|
-
t = @paused_tails[key][tail_id]
|
757
|
+
return unless @paused_tails.key?(key) && @paused_tails[key].key?(tail_id)
|
821
758
|
|
822
|
-
|
759
|
+
t = @paused_tails[key][tail_id]
|
823
760
|
|
824
|
-
|
825
|
-
@paused_tails.delete(key)
|
826
|
-
end
|
761
|
+
@paused_tails[key].delete(tail_id)
|
827
762
|
|
828
|
-
|
829
|
-
end
|
830
|
-
end
|
831
|
-
|
832
|
-
# Called by host functions to write memory to wasm instance so that
|
833
|
-
# the wasm module can read the result of a host function call
|
834
|
-
def write_to_memory(caller, res)
|
835
|
-
alloc = caller.export("alloc").to_func
|
836
|
-
memory = caller.export("memory").to_memory
|
837
|
-
|
838
|
-
# Serialize protobuf message
|
839
|
-
resp = res.to_proto
|
763
|
+
@paused_tails.delete(key) if @paused_tails[key].empty?
|
840
764
|
|
841
|
-
|
842
|
-
resp_ptr = alloc.call(resp.length)
|
843
|
-
|
844
|
-
# Write response to memory
|
845
|
-
memory.write(resp_ptr, resp)
|
846
|
-
|
847
|
-
# return 64bit integer where first 32 bits is the pointer, and the last 32 is the length
|
848
|
-
resp_ptr << 32 | resp.length
|
765
|
+
t
|
849
766
|
end
|
850
|
-
|
851
767
|
end
|
852
|
-
end
|
768
|
+
end
|
data/lib/tail.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
require "bozos_buckets"
|
1
|
+
require 'bozos_buckets'
|
3
2
|
|
4
3
|
NUM_TAIL_WORKERS = 2
|
5
4
|
MIN_TAIL_RESPONSE_INTERVAL_MS = 100
|
@@ -15,18 +14,19 @@ module Streamdal
|
|
15
14
|
@logger = log
|
16
15
|
@metrics = metrics
|
17
16
|
@active = active
|
18
|
-
@last_msg = Time
|
17
|
+
@last_msg = Time.at(0)
|
19
18
|
@queue = Queue.new
|
20
19
|
@workers = []
|
21
20
|
|
22
21
|
# Only use rate limiting if sample_options is set
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
22
|
+
return if request.sample_options.nil?
|
23
|
+
|
24
|
+
@limiter = BozosBuckets::Bucket.new(
|
25
|
+
initial_token_count: request.sample_options.sample_rate,
|
26
|
+
refill_rate: request.sample_options.sample_interval_seconds,
|
27
|
+
max_token_count: request.sample_options.sample_rate
|
28
|
+
)
|
29
|
+
|
30
30
|
end
|
31
31
|
|
32
32
|
def start_tail_workers
|
@@ -43,11 +43,8 @@ module Streamdal
|
|
43
43
|
sleep(1)
|
44
44
|
|
45
45
|
@workers.each do |worker|
|
46
|
-
if worker.alive?
|
47
|
-
worker.exit
|
48
|
-
end
|
46
|
+
worker.exit if worker.alive?
|
49
47
|
end
|
50
|
-
|
51
48
|
end
|
52
49
|
|
53
50
|
def start_tail_worker(worker_id)
|
@@ -63,35 +60,32 @@ module Streamdal
|
|
63
60
|
next
|
64
61
|
end
|
65
62
|
|
66
|
-
if Time
|
63
|
+
if Time.now - @last_msg < MIN_TAIL_RESPONSE_INTERVAL_MS
|
67
64
|
sleep(MIN_TAIL_RESPONSE_INTERVAL_MS)
|
68
65
|
@metrics.incr(Metrics::CounterEntry.new(COUNTER_DROPPED_TAIL_MESSAGES, nil, {}, 1))
|
69
66
|
@logger.debug("Dropped tail message for '#{@request.id}' due to rate limiting")
|
70
67
|
next
|
71
68
|
end
|
72
69
|
|
73
|
-
|
74
|
-
|
75
|
-
|
70
|
+
next if stub.nil?
|
71
|
+
|
72
|
+
tail_response = @queue.pop(false)
|
73
|
+
@logger.debug("Sending tail request for '#{tail_response.tail_request_id}'")
|
76
74
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
end
|
75
|
+
begin
|
76
|
+
stub.send_tail([tail_response], metadata: { 'auth-token' => @auth_token })
|
77
|
+
rescue Error => e
|
78
|
+
@logger.error("Error sending tail request: #{e}")
|
82
79
|
end
|
83
80
|
end
|
84
81
|
|
85
82
|
@logger.debug "Tail worker #{worker_id} exited"
|
86
|
-
|
87
83
|
end
|
88
84
|
|
89
85
|
def should_send
|
90
|
-
if @limiter.nil?
|
91
|
-
true
|
92
|
-
end
|
86
|
+
true if @limiter.nil?
|
93
87
|
|
94
88
|
@limiter.use_tokens(1)
|
95
89
|
end
|
96
90
|
end
|
97
|
-
end
|
91
|
+
end
|
data/lib/wasm_spec.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require 'steps/sp_steps_httprequest_pb'
|
3
|
+
require 'steps/sp_steps_detective_pb'
|
4
|
+
require 'steps/sp_steps_transform_pb'
|
5
|
+
require_relative 'streamdal'
|
6
|
+
|
7
|
+
class TestClient < Streamdal::Client
|
8
|
+
|
9
|
+
attr_accessor :kv
|
10
|
+
|
11
|
+
# Ignore rubocop warning
|
12
|
+
# rubocop:disable Lint/MissingSuper
|
13
|
+
def initialize
|
14
|
+
@cfg = {
|
15
|
+
step_timeout: 2
|
16
|
+
}
|
17
|
+
@functions = {}
|
18
|
+
|
19
|
+
logger = Logger.new($stdout)
|
20
|
+
logger.level = Logger::ERROR
|
21
|
+
|
22
|
+
@log = logger
|
23
|
+
@kv = Streamdal::KeyValue.new
|
24
|
+
@hostfunc = Streamdal::HostFunc.new(@kv)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec.describe 'WASM' do
|
29
|
+
let(:client) { TestClient.new }
|
30
|
+
|
31
|
+
context '_call_wasm' do
|
32
|
+
it 'raises an error if step is nil' do
|
33
|
+
expect { client.send(:_call_wasm, nil, nil, nil) }.to raise_error('step is required')
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'raises an error if data is nil' do
|
37
|
+
step = Streamdal::Protos::HttpRequestStep.new
|
38
|
+
expect { client.send(:_call_wasm, step, nil, nil) }.to raise_error('data is required')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'detective.wasm' do
|
43
|
+
before(:each) do
|
44
|
+
wasm_bytes = File.read(File.join(File.dirname(__FILE__), '..', 'test-assets', 'wasm', 'detective.wasm'))
|
45
|
+
|
46
|
+
@step = Streamdal::Protos::PipelineStep.new
|
47
|
+
@step.name = 'detective'
|
48
|
+
@step._wasm_bytes = wasm_bytes.b
|
49
|
+
@step._wasm_id = SecureRandom.uuid
|
50
|
+
@step._wasm_function = 'f'
|
51
|
+
@step.detective = Streamdal::Protos::DetectiveStep.new
|
52
|
+
@step.detective.path = 'object.field'
|
53
|
+
@step.detective.args = Google::Protobuf::RepeatedField.new(:string, ['streamdal'])
|
54
|
+
@step.detective.negate = false
|
55
|
+
@step.detective.type = :DETECTIVE_TYPE_STRING_CONTAINS_ANY
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'detects email in JSON payload' do
|
59
|
+
data = '{"object":{"field":"streamdal@gmail.com"}}'
|
60
|
+
|
61
|
+
res = client.send(:_call_wasm, @step, data, nil)
|
62
|
+
|
63
|
+
expect(res).not_to be_nil
|
64
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
65
|
+
expect(res.output_payload).to eq(data)
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'does not detect email in JSON payload' do
|
70
|
+
data = '{"object":{"field":"mark@gmail.com"}}'
|
71
|
+
|
72
|
+
res = client.send(:_call_wasm, @step, data, nil)
|
73
|
+
expect(res).not_to be_nil
|
74
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_FALSE)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context 'httprequest.wasm' do
|
79
|
+
before(:each) do
|
80
|
+
wasm_bytes = File.read(File.join(File.dirname(__FILE__), '..', 'test-assets', 'wasm', 'httprequest.wasm'))
|
81
|
+
|
82
|
+
http_req_step = Streamdal::Protos::HttpRequestStep.new
|
83
|
+
http_req_step.request = Streamdal::Protos::HttpRequest.new
|
84
|
+
http_req_step.request.url = 'https://www.google.com/404_me'
|
85
|
+
http_req_step.request.method = :HTTP_REQUEST_METHOD_GET
|
86
|
+
http_req_step.request.body = ''
|
87
|
+
|
88
|
+
@step = Streamdal::Protos::PipelineStep.new
|
89
|
+
@step.name = 'http request'
|
90
|
+
@step._wasm_bytes = wasm_bytes.b
|
91
|
+
@step._wasm_id = SecureRandom.uuid
|
92
|
+
@step._wasm_function = 'f'
|
93
|
+
@step.http_request = http_req_step
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'returns false on 404' do
|
97
|
+
res = client.send(:_call_wasm, @step, '', nil)
|
98
|
+
|
99
|
+
expect(res).not_to be_nil
|
100
|
+
expect(res.exit_msg).to eq('Request returned non-200 response code: 404')
|
101
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_FALSE)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
context 'inferschema.wasm' do
|
106
|
+
before(:each) do
|
107
|
+
wasm_bytes = File.read(File.join(File.dirname(__FILE__), '..', 'test-assets', 'wasm', 'inferschema.wasm'))
|
108
|
+
|
109
|
+
@step = Streamdal::Protos::PipelineStep.new
|
110
|
+
@step.name = 'schema inference'
|
111
|
+
@step._wasm_bytes = wasm_bytes.b
|
112
|
+
@step._wasm_id = SecureRandom.uuid
|
113
|
+
@step._wasm_function = 'f'
|
114
|
+
@step.infer_schema = Streamdal::Protos::InferSchemaStep.new
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'infers schema' do
|
118
|
+
payload = '{"object": {"payload": "test"}}'
|
119
|
+
res = client.send(:_call_wasm, @step, payload, nil)
|
120
|
+
|
121
|
+
expected_schema = '{"$schema":"http://json-schema.org/draft-07/schema#","properties":{"object":{"properties":{"payload":{"type":"string"}},"required":["payload"],"type":"object"}},"required":["object"],"type":"object"}'
|
122
|
+
|
123
|
+
expect(res).not_to be_nil
|
124
|
+
expect(res.exit_msg).to eq('inferred fresh schema')
|
125
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
126
|
+
expect(res.output_payload).to eq(payload)
|
127
|
+
expect(res.output_step).to eq(expected_schema)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context 'transform.wasm' do
|
132
|
+
before(:each) do
|
133
|
+
wasm_bytes = File.read(File.join(File.dirname(__FILE__), '..', 'test-assets', 'wasm', 'transform.wasm'))
|
134
|
+
|
135
|
+
@step = Streamdal::Protos::PipelineStep.new
|
136
|
+
@step.name = 'transform'
|
137
|
+
@step._wasm_bytes = wasm_bytes.b
|
138
|
+
@step._wasm_id = SecureRandom.uuid
|
139
|
+
@step._wasm_function = 'f'
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'deletes a field from a payload' do
|
143
|
+
@step.transform = Streamdal::Protos::TransformStep.new
|
144
|
+
@step.transform.type = :TRANSFORM_TYPE_DELETE_FIELD
|
145
|
+
@step.transform.delete_field_options = Streamdal::Protos::TransformDeleteFieldOptions.new
|
146
|
+
@step.transform.delete_field_options.paths = Google::Protobuf::RepeatedField.new(:string, ['object.another'])
|
147
|
+
|
148
|
+
payload = '{"object": {"payload": "old val", "another": "field"}}'
|
149
|
+
res = client.send(:_call_wasm, @step, payload, nil)
|
150
|
+
|
151
|
+
expect(res).not_to be_nil
|
152
|
+
expect(res.exit_msg).to eq('Successfully transformed payload')
|
153
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
154
|
+
expect(res.output_payload).to eq('{"object": {"payload": "old val"}}')
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'replaces a fields value with a new one' do
|
158
|
+
@step.transform = Streamdal::Protos::TransformStep.new
|
159
|
+
@step.transform.type = :TRANSFORM_TYPE_REPLACE_VALUE
|
160
|
+
@step.transform.replace_value_options = Streamdal::Protos::TransformReplaceValueOptions.new
|
161
|
+
@step.transform.replace_value_options.path = 'object.payload'
|
162
|
+
@step.transform.replace_value_options.value = '"new val"'
|
163
|
+
|
164
|
+
payload = '{"object": {"payload": "old val"}}'
|
165
|
+
res = client.send(:_call_wasm, @step, payload, nil)
|
166
|
+
|
167
|
+
expect(res).not_to be_nil
|
168
|
+
expect(res.exit_msg).to eq('Successfully transformed payload')
|
169
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
170
|
+
expect(res.output_payload).to eq('{"object": {"payload": "new val"}}')
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'truncates the value of a field' do
|
174
|
+
@step.transform = Streamdal::Protos::TransformStep.new
|
175
|
+
@step.transform.type = :TRANSFORM_TYPE_TRUNCATE_VALUE
|
176
|
+
@step.transform.truncate_options = Streamdal::Protos::TransformTruncateOptions.new
|
177
|
+
@step.transform.truncate_options.type = :TRANSFORM_TRUNCATE_TYPE_LENGTH
|
178
|
+
@step.transform.truncate_options.path = 'object.payload'
|
179
|
+
@step.transform.truncate_options.value = 3
|
180
|
+
|
181
|
+
payload = '{"object": {"payload": "old val"}}'
|
182
|
+
res = client.send(:_call_wasm, @step, payload, nil)
|
183
|
+
|
184
|
+
expect(res).not_to be_nil
|
185
|
+
expect(res.exit_msg).to eq('Successfully transformed payload')
|
186
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
187
|
+
expect(res.output_payload).to eq('{"object": {"payload": "old"}}')
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'performs dynamic transformation' do
|
191
|
+
# TODO: add this test
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context 'validjson.wasm' do
|
196
|
+
before(:each) do
|
197
|
+
wasm_bytes = File.read(File.join(File.dirname(__FILE__), '..', 'test-assets', 'wasm', 'validjson.wasm'))
|
198
|
+
|
199
|
+
@step = Streamdal::Protos::PipelineStep.new
|
200
|
+
@step.name = 'validate json'
|
201
|
+
@step._wasm_bytes = wasm_bytes.b
|
202
|
+
@step._wasm_id = SecureRandom.uuid
|
203
|
+
@step._wasm_function = 'f'
|
204
|
+
@step.valid_json = Streamdal::Protos::ValidJSONStep.new
|
205
|
+
|
206
|
+
end
|
207
|
+
|
208
|
+
it 'validates a valid JSON payload' do
|
209
|
+
|
210
|
+
payload = '{"object": {"payload": "test"}}'
|
211
|
+
res = client.send(:_call_wasm, @step, payload, nil)
|
212
|
+
|
213
|
+
expect(res).not_to be_nil
|
214
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
215
|
+
expect(res.output_payload).to eq(payload)
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'returns false on invalid JSON payload' do
|
219
|
+
payload = '{"object": {"payload": "test"'
|
220
|
+
res = client.send(:_call_wasm, @step, payload, nil)
|
221
|
+
|
222
|
+
expect(res).not_to be_nil
|
223
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_FALSE)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
context 'kv.wasm' do
|
228
|
+
before(:each) do
|
229
|
+
wasm_bytes = File.read(File.join(File.dirname(__FILE__), '..', 'test-assets', 'wasm', 'kv.wasm'))
|
230
|
+
|
231
|
+
client.kv.set('test', 'test')
|
232
|
+
|
233
|
+
@step = Streamdal::Protos::PipelineStep.new
|
234
|
+
@step.name = 'kv exists'
|
235
|
+
@step._wasm_bytes = wasm_bytes.b
|
236
|
+
@step._wasm_id = SecureRandom.uuid
|
237
|
+
@step._wasm_function = 'f'
|
238
|
+
@step.kv = Streamdal::Protos::KVStep.new
|
239
|
+
@step.kv.key = 'test'
|
240
|
+
@step.kv.mode = :KV_MODE_STATIC
|
241
|
+
@step.kv.action = :KV_ACTION_EXISTS
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'returns true if a key exists' do
|
245
|
+
res = client.send(:_call_wasm, @step, '', nil)
|
246
|
+
|
247
|
+
expect(res).not_to be_nil
|
248
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_TRUE)
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'returns false when a key doesnt exist' do
|
252
|
+
@step.kv.key = 'not_exists'
|
253
|
+
res = client.send(:_call_wasm, @step, '', nil)
|
254
|
+
|
255
|
+
expect(res).not_to be_nil
|
256
|
+
expect(res.exit_code).to eq(:WASM_EXIT_CODE_FALSE)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: streamdal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mark Gregan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description:
|
14
14
|
email: mark@streamdal.com
|
@@ -33,6 +33,7 @@ files:
|
|
33
33
|
- lib/tail_spec.rb
|
34
34
|
- lib/validation.rb
|
35
35
|
- lib/validation_spec.rb
|
36
|
+
- lib/wasm_spec.rb
|
36
37
|
homepage: https://docs.streamdal.com
|
37
38
|
licenses:
|
38
39
|
- Apache-2.0
|
@@ -45,7 +46,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
45
46
|
requirements:
|
46
47
|
- - '='
|
47
48
|
- !ruby/object:Gem::Version
|
48
|
-
version: 0.0.
|
49
|
+
version: 0.0.3
|
49
50
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
51
|
requirements:
|
51
52
|
- - ">="
|