streamdal 0.0.1 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="