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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cdf556c16ea4239101cc21f803b78c458c5e2490a1c5df6b2211f5268cc94799
4
- data.tar.gz: 1bca44db3048358a5bdf978504a8559c1a81ede836a11269c5ab8020698d71b6
3
+ metadata.gz: ff825b88dbb81240b3ea5c3fd2cf264a88ecdafbb197ba886ed11acfe098ab93
4
+ data.tar.gz: 11a9e57851f4296c715b393d8c3ca7f33506d0050e6a4bf74121f92c141288c4
5
5
  SHA512:
6
- metadata.gz: 9f0fb7a83fc6e924f520d5402385bd857aff3a27884fd2a1aa9572a002723ad26d3849a2930829e6a05e450547ab1e80293992a693bbeb2bb9bacad8756011f9
7
- data.tar.gz: 2176e44cd407be37c0137f48c77f7a56550cddacc36a5a8043643049b416138c1b96b35c850c630dfd1f9febdc4e72e9224fb055b8dfd871c54e9bef5fbdbe47
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 "steps/sp_steps_kv_pb"
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("memory").to_memory.read(ptr, len)
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("memory").to_memory.read(ptr, len)
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::HttpRequest.decode(data)
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
- wasm_resp = Streamdal::Protos::HttpResponse.new
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
- # Successful request, build the proto from the httparty response
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 = response.code
57
- wasm_resp.body = response.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
- response.headers.each do |k, values|
63
- wasm_resp.headers[k] = values.kind_of?(Array) ? values.join("; ") : 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
- private
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": "application/json", },
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 = req.body
125
+ options.body = body
91
126
  return HTTParty.post(req.url, options)
92
127
  when :HTTP_REQUEST_METHOD_PUT
93
- options.body = req.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 = req.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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Streamdal
2
4
  class KeyValue
3
5
  def initialize
@@ -49,4 +51,4 @@ module Streamdal
49
51
  end
50
52
  end
51
53
  end
52
- end
54
+ end
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 = 0
10
- @last_updated = Time::now
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 = @value + val
17
- @last_updated = Time::now
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 = "counter_consume_bytes"
37
- COUNTER_CONSUME_PROCESSED = "counter_consume_processed"
38
- COUNTER_CONSUME_ERRORS = "counter_consume_errors"
39
- COUNTER_PRODUCE_BYTES = "counter_produce_bytes"
40
- COUNTER_PRODUCE_PROCESSED = "counter_produce_processed"
41
- COUNTER_PRODUCE_ERRORS = "counter_produce_errors"
42
- COUNTER_NOTIFY = "counter_notify"
43
- COUNTER_DROPPED_TAIL_MESSAGES = "counter_dropped_tail_messages"
44
- COUNTER_CONSUME_BYTES_RATE = "counter_consume_bytes_rate"
45
- COUNTER_PRODUCE_BYTES_RATE = "counter_produce_bytes_rate"
46
- COUNTER_CONSUME_PROCESSED_RATE = "counter_consume_processed_rate"
47
- COUNTER_PRODUCE_PROCESSED_RATE = "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
- labels = {}
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::composite_id(ce.name, ce.labels)
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::composite_id(ce.name, ce.labels)] = c
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
- unless @exit
170
- @log.debug("Starting publisher")
161
+ return if @exit
171
162
 
172
- # Sleep on startup and then and between each loop run
173
- sleep(DEFAULT_COUNTER_PUBLISH_INTERVAL)
163
+ @log.debug('Starting publisher')
174
164
 
175
- # Get all counters
176
- # Loop over each counter, get the value,
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
- new_counters.each do |_, counter|
185
- if counter.val == 0
186
- next
187
- end
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
- ce = CounterEntry.new(counter.name, counter.aud, counter.labels, counter.val)
190
- counter.reset
177
+ new_counters.each_value do |counter|
178
+ next if counter.val.zero?
191
179
 
192
- @publish_queue.push(ce)
193
- end
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
- next
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("Starting reaper")
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 > 0
230
- next
231
- end
218
+ next if counter.val.positive?
232
219
 
233
- if Time::now - counter.last_updated > DEFAULT_COUNTER_TTL
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("Exiting reaper")
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
- { "auth-token" => @cfg[:streamdal_token].to_s }
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 "sp_internal_services_pb"
11
+ require 'sp_internal_services_pb'
12
12
  require 'sp_pipeline_pb'
13
- require "sp_wsm_pb"
14
- require "steps/sp_steps_httprequest_pb"
15
- require "steps/sp_steps_kv_pb"
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.length == 0
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 = "payload size exceeds maximum allowed size"
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.length == 0
178
- _send_tail(aud, "", original_data, original_data)
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 > 0
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, "", original_data, resp.data)
286
+ _send_tail(aud, '', original_data, resp.data)
300
287
 
301
288
  if @cfg[:dry_run]
302
- @log.debug "Dry-run, setting response data to original data"
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(STDOUT)
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 "kv"
318
+ when 'kv'
342
319
  _handle_kv(cmd)
343
- when "tail"
320
+ when 'tail'
344
321
  _handle_tail_request(cmd)
345
- when "set_pipelines"
322
+ when 'set_pipelines'
346
323
  _set_pipelines(cmd)
347
- when "keep_alive"
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("env", "httpRequest", [:i32, :i32], [:i64]) do |caller, ptr, len|
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("env", "kvExists", [:i32, :i32], [:i64]) do |caller, ptr, len|
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
- return resp
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 = "ruby-sdk"
504
- ci.library_version = "0.0.1"
505
- ci.language = "ruby"
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
- { "auth-token" => @cfg[:streamdal_token].to_s }
479
+ { 'auth-token' => @cfg[:streamdal_token].to_s }
515
480
  end
516
481
 
517
482
  def _register
518
- @log.info("register started")
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("register exited")
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("memory").to_memory
543
- alloc = wasm_func.instance.export("alloc").to_func
544
- dealloc = wasm_func.instance.export("dealloc").to_func
545
- f = wasm_func.instance.export("f").to_func
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.each do |_, aud|
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.length == 0
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 "Notifying"
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].length == 0
743
- @tails.delete(key)
744
- end
688
+ @tails.delete(key) if @tails[key].empty?
745
689
  end
746
690
 
747
- if @paused_tails.key?(key) && @paused_tails[key].key?(cmd.tail.request.id)
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
- if @paused_tails[key].length == 0
751
- @paused_tails.delete(key)
752
- end
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].length == 0
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
- if @tails.key?(key) && @tails[key].key?(tail_id)
803
- t = @tails[key][tail_id]
804
- t.stop_tail
742
+ return unless @tails.key?(key) && @tails[key].key?(tail_id)
805
743
 
806
- @tails[key].delete(tail_id)
744
+ t = @tails[key][tail_id]
745
+ t.stop_tail
807
746
 
808
- if @tails[key].length == 0
809
- @tails.delete(key)
810
- end
747
+ @tails[key].delete(tail_id)
811
748
 
812
- t
813
- end
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
- if @paused_tails.key?(key) && @paused_tails[key].key?(tail_id)
820
- t = @paused_tails[key][tail_id]
757
+ return unless @paused_tails.key?(key) && @paused_tails[key].key?(tail_id)
821
758
 
822
- @paused_tails[key].delete(tail_id)
759
+ t = @paused_tails[key][tail_id]
823
760
 
824
- if @paused_tails[key].length == 0
825
- @paused_tails.delete(key)
826
- end
761
+ @paused_tails[key].delete(tail_id)
827
762
 
828
- t
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
- # Allocate memory for response
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
- # TODO: implement token bucket limiter
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::at(0)
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
- unless request.sample_options.nil?
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
- end
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::now - @last_msg < MIN_TAIL_RESPONSE_INTERVAL_MS
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
- unless stub.nil?
74
- tail_response = @queue.pop(non_block = false)
75
- @logger.debug("Sending tail request for '#{tail_response.tail_request_id}'")
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
- begin
78
- stub.send_tail([tail_response], metadata: { "auth-token" => @auth_token })
79
- rescue => e
80
- @logger.error("Error sending tail request: #{e}")
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.1
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-05-03 00:00:00.000000000 Z
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.1
49
+ version: 0.0.3
49
50
  required_rubygems_version: !ruby/object:Gem::Requirement
50
51
  requirements:
51
52
  - - ">="