volcano-sdk 0.31.0 → 0.32.1
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/README.md +13 -7
- data/lib/volcano/database.rb +28 -37
- data/lib/volcano/durable.rb +6 -6
- data/lib/volcano/durable_models.rb +65 -22
- data/lib/volcano/function_auth.rb +9 -2
- data/lib/volcano/functions.rb +18 -15
- data/lib/volcano/immutable_request_value.rb +21 -0
- data/lib/volcano/logs.rb +14 -9
- data/lib/volcano/models.rb +55 -22
- data/lib/volcano/realtime/broadcast_delivery.rb +26 -2
- data/lib/volcano/realtime/channel_callbacks.rb +23 -15
- data/lib/volcano/realtime/channel_lifecycle.rb +9 -9
- data/lib/volcano/realtime/connection.rb +9 -5
- data/lib/volcano/realtime/connection_callbacks.rb +5 -1
- data/lib/volcano/realtime/lifecycle.rb +2 -1
- data/lib/volcano/realtime/postgres_batch.rb +11 -3
- data/lib/volcano/realtime/postgres_changes.rb +33 -16
- data/lib/volcano/realtime/postgres_database.rb +14 -1
- data/lib/volcano/realtime/postgres_delivery.rb +6 -1
- data/lib/volcano/realtime/postgres_expansion.rb +10 -4
- data/lib/volcano/realtime/presence.rb +18 -4
- data/lib/volcano/realtime/presence_state.rb +23 -6
- data/lib/volcano/realtime/protocol.rb +14 -9
- data/lib/volcano/realtime/protocol_dispatch.rb +25 -7
- data/lib/volcano/realtime/protocol_io.rb +8 -4
- data/lib/volcano/realtime/protocol_lifecycle.rb +2 -0
- data/lib/volcano/realtime/protocol_publication_dispatch.rb +12 -10
- data/lib/volcano/realtime/protocol_recovery.rb +7 -3
- data/lib/volcano/realtime/protocol_recovery_dispatch.rb +2 -0
- data/lib/volcano/realtime/reconnect.rb +4 -1
- data/lib/volcano/realtime.rb +122 -99
- data/lib/volcano/session.rb +14 -3
- data/lib/volcano/transport.rb +53 -11
- data/lib/volcano/user.rb +12 -7
- data/lib/volcano/version.rb +1 -1
- data/lib/volcano.rb +1 -0
- data/sig/auth_session_records.rbs +1 -1
- data/sig/client.rbs +15 -0
- data/sig/core_facades.rbs +81 -0
- data/sig/durable_records.rbs +1 -1
- data/sig/locks.rbs +10 -0
- data/sig/realtime.rbs +45 -0
- data/sig/realtime_postgres_change.rbs +1 -2
- data/sig/user.rbs +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b0f776d6ce8242c9e13c9bda50c0a57656288bfe4ff5e14f88e75c19a7609ded
|
|
4
|
+
data.tar.gz: 25436052f65e6ededb49df00688a2e7d4076a233ca84502a2503dc470d1ab6d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0da1887ccafe797e2169899402489b9eab52d6587782461fd0390dfc63041ebe0a134e6ab89dc42d9ab5e5acf2dae34332516b2727f4bc51726f078319d58205
|
|
7
|
+
data.tar.gz: fce88e25d637dfb1dda8bb10b279e0dbfcbe621e4912ca1c941460eb1e94b6cc4b1e84242402b692ba19a66c0cab89992ea14d826beef3710629970dd361353f
|
data/README.md
CHANGED
|
@@ -505,26 +505,32 @@ handle = client.durable.start(
|
|
|
505
505
|
execution_name: "order-9"
|
|
506
506
|
)
|
|
507
507
|
|
|
508
|
-
|
|
508
|
+
owner_client = Volcano::Client.new(
|
|
509
|
+
api_url: "https://api.volcano.dev",
|
|
510
|
+
anon_key: ENV.fetch("VOLCANO_ANON_KEY"),
|
|
511
|
+
access_token: ENV.fetch("VOLCANO_PLATFORM_TOKEN")
|
|
512
|
+
)
|
|
513
|
+
execution = owner_client.durable.get(project_id, "charge-order", handle.id)
|
|
509
514
|
puts [execution.status, execution.terminal?, execution.result]
|
|
510
515
|
|
|
511
|
-
page =
|
|
516
|
+
page = owner_client.durable.list(project_id, "charge-order", status: "running")
|
|
512
517
|
puts [page.total, page.has_more]
|
|
513
518
|
|
|
514
|
-
|
|
519
|
+
owner_client.durable.stop(project_id, "charge-order", handle.id)
|
|
515
520
|
```
|
|
516
521
|
|
|
517
522
|
`start` takes a durable function's name or its id and returns a handle, never a
|
|
518
|
-
result: an execution can run for
|
|
523
|
+
result: an execution can run for up to 366 days, so its result is read back with `get`.
|
|
519
524
|
It uses the same credential `invoke` does — the active user session, then a
|
|
520
525
|
configured service key, then the anonymous key — and is the only durable
|
|
521
526
|
operation an application credential may perform. An `execution_name` makes the
|
|
522
527
|
start idempotent: starting again under the same name returns the execution that
|
|
523
528
|
already exists rather than beginning a second one.
|
|
524
529
|
|
|
525
|
-
`get`, `list`, and `stop` are owner-scoped and require
|
|
526
|
-
|
|
527
|
-
|
|
530
|
+
`get`, `list`, and `stop` are owner-scoped and require the project owner's
|
|
531
|
+
platform user token, because an execution is addressed by its id alone.
|
|
532
|
+
Auth-user sessions, anonymous keys, service keys, and project access tokens are
|
|
533
|
+
not accepted. `get` carries the deeply
|
|
528
534
|
frozen `result` once the execution has succeeded, and `error` when it failed;
|
|
529
535
|
`result_expired` separates a result the platform has discarded from a function
|
|
530
536
|
that returned nothing. `terminal?` reports whether the execution has stopped
|
data/lib/volcano/database.rb
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Ruby SDK runtime and immutable database builders.
|
|
3
4
|
module Volcano
|
|
4
|
-
# Captures caller-owned query values before storing them in immutable builders.
|
|
5
|
-
module ImmutableQueryValue
|
|
6
|
-
module_function
|
|
7
|
-
|
|
8
|
-
def capture(value)
|
|
9
|
-
case value
|
|
10
|
-
when String then value.dup.freeze
|
|
11
|
-
when Array then value.map { |item| capture(item) }.freeze
|
|
12
|
-
when Hash then capture_hash(value)
|
|
13
|
-
else value
|
|
14
|
-
end
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def capture_hash(value)
|
|
18
|
-
value.to_h { |key, item| [capture(key), capture(item)] }.freeze
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
private_constant :ImmutableQueryValue
|
|
22
|
-
|
|
23
5
|
QueryContext = Data.define(:client, :transport, :database_name)
|
|
24
6
|
private_constant :QueryContext
|
|
25
7
|
QueryState = Data.define(:columns, :filters, :order, :limit, :offset)
|
|
26
8
|
private_constant :QueryState
|
|
9
|
+
# @type var empty_columns: Array[String]
|
|
10
|
+
empty_columns = []
|
|
11
|
+
empty_columns.freeze
|
|
12
|
+
# @type var empty_conditions: Array[query_condition]
|
|
13
|
+
empty_conditions = []
|
|
14
|
+
empty_conditions.freeze
|
|
27
15
|
EMPTY_QUERY_STATE = QueryState.new(
|
|
28
|
-
columns:
|
|
29
|
-
filters:
|
|
30
|
-
order:
|
|
16
|
+
columns: empty_columns,
|
|
17
|
+
filters: empty_conditions,
|
|
18
|
+
order: empty_conditions,
|
|
31
19
|
limit: nil,
|
|
32
20
|
offset: nil
|
|
33
21
|
)
|
|
@@ -35,6 +23,7 @@ module Volcano
|
|
|
35
23
|
|
|
36
24
|
# Adds the shared immutable filter vocabulary to database builders.
|
|
37
25
|
module FilterMethods
|
|
26
|
+
# @dynamic copy
|
|
38
27
|
def eq(column, value) = add_filter(column, 'eq', value)
|
|
39
28
|
def neq(column, value) = add_filter(column, 'neq', value)
|
|
40
29
|
def gt(column, value) = add_filter(column, 'gt', value)
|
|
@@ -50,9 +39,9 @@ module Volcano
|
|
|
50
39
|
|
|
51
40
|
def add_filter(column, operator, value)
|
|
52
41
|
condition = {
|
|
53
|
-
'column' =>
|
|
42
|
+
'column' => ImmutableRequestValue.capture(column),
|
|
54
43
|
'operator' => operator,
|
|
55
|
-
'value' =>
|
|
44
|
+
'value' => ImmutableRequestValue.capture(value)
|
|
56
45
|
}.freeze
|
|
57
46
|
copy(filters: [*@filters, condition])
|
|
58
47
|
end
|
|
@@ -62,7 +51,7 @@ module Volcano
|
|
|
62
51
|
# Creates immutable queries scoped to one project database.
|
|
63
52
|
class Database
|
|
64
53
|
def initialize(client, transport, name)
|
|
65
|
-
database_name =
|
|
54
|
+
database_name = ImmutableRequestValue.capture(name)
|
|
66
55
|
@context = QueryContext.new(client:, transport:, database_name:)
|
|
67
56
|
end
|
|
68
57
|
|
|
@@ -77,7 +66,7 @@ module Volcano
|
|
|
77
66
|
|
|
78
67
|
def initialize(context, table, state: EMPTY_QUERY_STATE)
|
|
79
68
|
@context = context
|
|
80
|
-
@table =
|
|
69
|
+
@table = ImmutableRequestValue.capture(table)
|
|
81
70
|
@columns = state.columns
|
|
82
71
|
@filters = state.filters
|
|
83
72
|
@order = state.order
|
|
@@ -87,13 +76,13 @@ module Volcano
|
|
|
87
76
|
end
|
|
88
77
|
|
|
89
78
|
def select(*columns)
|
|
90
|
-
copy(columns:
|
|
79
|
+
copy(columns: ImmutableRequestValue.capture(columns))
|
|
91
80
|
end
|
|
92
81
|
|
|
93
|
-
def insert(values) = InsertBuilder.new(@context, @table,
|
|
82
|
+
def insert(values) = InsertBuilder.new(@context, @table, ImmutableRequestValue.capture(values))
|
|
94
83
|
|
|
95
84
|
def update(values)
|
|
96
|
-
UpdateBuilder.new(@context, @table,
|
|
85
|
+
UpdateBuilder.new(@context, @table, ImmutableRequestValue.capture(values), filters: @filters)
|
|
97
86
|
end
|
|
98
87
|
|
|
99
88
|
def delete
|
|
@@ -101,7 +90,7 @@ module Volcano
|
|
|
101
90
|
end
|
|
102
91
|
|
|
103
92
|
def order(column, ascending: true)
|
|
104
|
-
clause = { 'column' =>
|
|
93
|
+
clause = { 'column' => ImmutableRequestValue.capture(column), 'ascending' => ascending }.freeze
|
|
105
94
|
copy(order: [*@order, clause])
|
|
106
95
|
end
|
|
107
96
|
|
|
@@ -123,7 +112,7 @@ module Volcano
|
|
|
123
112
|
)
|
|
124
113
|
end
|
|
125
114
|
end
|
|
126
|
-
Transport.body(response, 200)
|
|
115
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
127
116
|
end
|
|
128
117
|
|
|
129
118
|
private
|
|
@@ -149,7 +138,9 @@ module Volcano
|
|
|
149
138
|
end
|
|
150
139
|
|
|
151
140
|
def query_body
|
|
152
|
-
|
|
141
|
+
# @type var body: sdk_payload
|
|
142
|
+
body = { 'table' => @table }
|
|
143
|
+
body.tap do |body|
|
|
153
144
|
add_query_selection(body)
|
|
154
145
|
body['limit'] = @limit unless @limit.nil?
|
|
155
146
|
body['offset'] = @offset unless @offset.nil?
|
|
@@ -184,7 +175,7 @@ module Volcano
|
|
|
184
175
|
)
|
|
185
176
|
end
|
|
186
177
|
end
|
|
187
|
-
Transport.body(response, 200)
|
|
178
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
188
179
|
end
|
|
189
180
|
end
|
|
190
181
|
|
|
@@ -192,7 +183,7 @@ module Volcano
|
|
|
192
183
|
class UpdateBuilder
|
|
193
184
|
include FilterMethods
|
|
194
185
|
|
|
195
|
-
def initialize(context, table, values, filters:
|
|
186
|
+
def initialize(context, table, values, filters: EMPTY_QUERY_STATE.filters)
|
|
196
187
|
@context = context
|
|
197
188
|
@table = table
|
|
198
189
|
@values = values
|
|
@@ -210,7 +201,7 @@ module Volcano
|
|
|
210
201
|
)
|
|
211
202
|
end
|
|
212
203
|
end
|
|
213
|
-
Transport.body(response, 200)
|
|
204
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
214
205
|
end
|
|
215
206
|
|
|
216
207
|
private
|
|
@@ -224,7 +215,7 @@ module Volcano
|
|
|
224
215
|
class DeleteBuilder
|
|
225
216
|
include FilterMethods
|
|
226
217
|
|
|
227
|
-
def initialize(context, table, filters:
|
|
218
|
+
def initialize(context, table, filters: EMPTY_QUERY_STATE.filters)
|
|
228
219
|
@context = context
|
|
229
220
|
@table = table
|
|
230
221
|
@filters = filters
|
|
@@ -241,7 +232,7 @@ module Volcano
|
|
|
241
232
|
)
|
|
242
233
|
end
|
|
243
234
|
end
|
|
244
|
-
Transport.body(response, 200)
|
|
235
|
+
Transport.json_rows(Transport.body(response, 200))
|
|
245
236
|
end
|
|
246
237
|
|
|
247
238
|
private
|
data/lib/volcano/durable.rb
CHANGED
|
@@ -40,10 +40,10 @@ module Volcano
|
|
|
40
40
|
|
|
41
41
|
# Reads an execution, including its result once it has succeeded.
|
|
42
42
|
#
|
|
43
|
-
# Owner-scoped: an execution is addressed by its id alone
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
#
|
|
43
|
+
# Owner-scoped: an execution is addressed by its id alone, so this takes
|
|
44
|
+
# the project id and the project owner's platform user token. Auth-user
|
|
45
|
+
# sessions, anon keys, service keys, and project access tokens are not
|
|
46
|
+
# accepted. Poll it from a backend, not a browser.
|
|
47
47
|
def get(project_id, function_name, execution_id)
|
|
48
48
|
project, function_id, execution = execution_path(project_id, function_name, execution_id)
|
|
49
49
|
response = Transport.invoke do
|
|
@@ -100,7 +100,7 @@ module Volcano
|
|
|
100
100
|
# An empty path segment would address the collection instead of the
|
|
101
101
|
# execution, which is a different request rather than a failed one.
|
|
102
102
|
def identifier(value, field)
|
|
103
|
-
raise ArgumentError, "#{field} must be a non-empty String" unless present_string?(value)
|
|
103
|
+
raise ArgumentError, "#{field} must be a non-empty String" unless value.is_a?(String) && present_string?(value)
|
|
104
104
|
|
|
105
105
|
value.strip.freeze
|
|
106
106
|
end
|
|
@@ -128,7 +128,7 @@ module Volcano
|
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
def validate_page_limit(limit)
|
|
131
|
-
return if limit.nil? || (positive_integer?(limit) && limit <= MAX_PAGE_SIZE)
|
|
131
|
+
return if limit.nil? || (limit.is_a?(Integer) && positive_integer?(limit) && limit <= MAX_PAGE_SIZE)
|
|
132
132
|
|
|
133
133
|
raise ArgumentError, "limit must be an Integer between 1 and #{MAX_PAGE_SIZE}"
|
|
134
134
|
end
|
|
@@ -15,9 +15,16 @@ module Volcano
|
|
|
15
15
|
private_constant :DURABLE_TERMINAL_STATUSES
|
|
16
16
|
|
|
17
17
|
# Why a failed or timed-out execution ended.
|
|
18
|
-
DurableExecutionError = Data.define(:type, :message)
|
|
18
|
+
DurableExecutionError = Data.define(:type, :message)
|
|
19
|
+
|
|
20
|
+
# Reopens the generated record for checked initialization.
|
|
21
|
+
class DurableExecutionError
|
|
22
|
+
# @dynamic type, message, members, with, to_h, deconstruct, deconstruct_keys
|
|
23
|
+
# @dynamic self.[], self.members
|
|
19
24
|
def initialize(type: nil, message: nil)
|
|
20
|
-
|
|
25
|
+
type = type&.dup&.freeze
|
|
26
|
+
message = message&.dup&.freeze
|
|
27
|
+
super
|
|
21
28
|
end
|
|
22
29
|
end
|
|
23
30
|
|
|
@@ -27,7 +34,13 @@ module Volcano
|
|
|
27
34
|
# once its retention lapses. That is not the same as a function that returned
|
|
28
35
|
# nothing, so read `result_expired` before concluding anything from a missing
|
|
29
36
|
# result.
|
|
30
|
-
DurableExecution = Data.define(*DURABLE_EXECUTION_ATTRIBUTES)
|
|
37
|
+
DurableExecution = Data.define(*DURABLE_EXECUTION_ATTRIBUTES)
|
|
38
|
+
|
|
39
|
+
# Reopens the generated record for checked initialization.
|
|
40
|
+
class DurableExecution
|
|
41
|
+
# @dynamic id, function_id, name, status, region, created_at, result, result_expired
|
|
42
|
+
# @dynamic error, completed_at, members, with, to_h, deconstruct, deconstruct_keys
|
|
43
|
+
# @dynamic self.[], self.members
|
|
31
44
|
def initialize(**attributes)
|
|
32
45
|
unknown = attributes.keys - DURABLE_EXECUTION_ATTRIBUTES
|
|
33
46
|
raise ArgumentError, "unknown keywords: #{unknown.join(', ')}" unless unknown.empty?
|
|
@@ -38,7 +51,8 @@ module Volcano
|
|
|
38
51
|
values = DURABLE_EXECUTION_ATTRIBUTES.to_h do |name|
|
|
39
52
|
[name, immutable_value(attributes[name])]
|
|
40
53
|
end
|
|
41
|
-
|
|
54
|
+
attributes = values
|
|
55
|
+
super
|
|
42
56
|
end
|
|
43
57
|
|
|
44
58
|
# Reports whether the execution has stopped changing.
|
|
@@ -64,12 +78,15 @@ module Volcano
|
|
|
64
78
|
end
|
|
65
79
|
|
|
66
80
|
# One page of a durable function's executions, most recent first.
|
|
67
|
-
DurableExecutionPage = Data.define(:executions, :page, :limit, :total, :has_more)
|
|
81
|
+
DurableExecutionPage = Data.define(:executions, :page, :limit, :total, :has_more)
|
|
82
|
+
|
|
83
|
+
# Reopens the generated record for checked initialization.
|
|
84
|
+
class DurableExecutionPage
|
|
85
|
+
# @dynamic executions, page, limit, total, has_more, members, with, to_h
|
|
86
|
+
# @dynamic deconstruct, deconstruct_keys, self.[], self.members
|
|
68
87
|
def initialize(executions:, page:, limit:, total:, has_more:)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
page: page, limit: limit, total: total, has_more: has_more
|
|
72
|
-
)
|
|
88
|
+
executions = executions.to_a.dup.freeze
|
|
89
|
+
super
|
|
73
90
|
end
|
|
74
91
|
end
|
|
75
92
|
|
|
@@ -77,18 +94,17 @@ module Volcano
|
|
|
77
94
|
module DurableResponses
|
|
78
95
|
INCOMPLETE_EXECUTION = 'Expected a complete durable execution'
|
|
79
96
|
INCOMPLETE_PAGE = 'Expected a complete durable execution page'
|
|
80
|
-
|
|
81
|
-
private_constant :INCOMPLETE_EXECUTION, :INCOMPLETE_PAGE, :EXECUTION_FIELDS
|
|
97
|
+
private_constant :INCOMPLETE_EXECUTION, :INCOMPLETE_PAGE
|
|
82
98
|
|
|
83
99
|
private
|
|
84
100
|
|
|
85
101
|
def durable_execution(payload)
|
|
86
102
|
values = execution_payload(payload)
|
|
87
103
|
DurableExecution.new(
|
|
88
|
-
id: values
|
|
89
|
-
name: values
|
|
90
|
-
region: values
|
|
91
|
-
result: values['result'], result_expired: values['result_expired'],
|
|
104
|
+
id: execution_text(values, 'id'), function_id: execution_text(values, 'function_id'),
|
|
105
|
+
name: execution_text(values, 'name'), status: execution_text(values, 'status'),
|
|
106
|
+
region: execution_text(values, 'region'), created_at: execution_time(values),
|
|
107
|
+
result: execution_result(values['result']), result_expired: execution_expired(values['result_expired']),
|
|
92
108
|
error: execution_error(values['error']), completed_at: parse_time(values['completed_at'])
|
|
93
109
|
)
|
|
94
110
|
end
|
|
@@ -96,12 +112,36 @@ module Volcano
|
|
|
96
112
|
def execution_payload(payload)
|
|
97
113
|
raise TypeError, INCOMPLETE_EXECUTION unless payload.is_a?(Hash)
|
|
98
114
|
|
|
99
|
-
complete = EXECUTION_FIELDS.all? { |field| present_string?(payload[field]) }
|
|
100
|
-
raise TypeError, INCOMPLETE_EXECUTION unless complete && payload['created_at']
|
|
101
|
-
|
|
102
115
|
payload
|
|
103
116
|
end
|
|
104
117
|
|
|
118
|
+
def execution_text(values, field)
|
|
119
|
+
value = values[field]
|
|
120
|
+
raise TypeError, INCOMPLETE_EXECUTION unless value.is_a?(String) && !value.strip.empty?
|
|
121
|
+
|
|
122
|
+
value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def execution_time(values)
|
|
126
|
+
case (value = values['created_at'])
|
|
127
|
+
when String then Time.iso8601(value)
|
|
128
|
+
when Time then value
|
|
129
|
+
else raise TypeError, INCOMPLETE_EXECUTION
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def execution_expired(value)
|
|
134
|
+
return value if value.nil? || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
135
|
+
|
|
136
|
+
raise TypeError, INCOMPLETE_EXECUTION
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def execution_result(value)
|
|
140
|
+
Transport.json_value(value)
|
|
141
|
+
rescue TypeError
|
|
142
|
+
raise TypeError, INCOMPLETE_EXECUTION, cause: nil
|
|
143
|
+
end
|
|
144
|
+
|
|
105
145
|
def execution_error(payload)
|
|
106
146
|
return if payload.nil?
|
|
107
147
|
raise TypeError, INCOMPLETE_EXECUTION unless payload.is_a?(Hash)
|
|
@@ -110,10 +150,10 @@ module Volcano
|
|
|
110
150
|
end
|
|
111
151
|
|
|
112
152
|
def durable_execution_page(payload)
|
|
113
|
-
data, has_more = page_payload(payload)
|
|
153
|
+
values, data, has_more = page_payload(payload)
|
|
114
154
|
DurableExecutionPage.new(
|
|
115
155
|
executions: data.map { |entry| durable_execution(entry) }, has_more: has_more,
|
|
116
|
-
page: count(
|
|
156
|
+
page: count(values['page']), limit: count(values['limit']), total: count(values['total'])
|
|
117
157
|
)
|
|
118
158
|
end
|
|
119
159
|
|
|
@@ -124,7 +164,7 @@ module Volcano
|
|
|
124
164
|
has_more = payload['has_more']
|
|
125
165
|
raise TypeError, INCOMPLETE_PAGE unless data.is_a?(Array) && [true, false].include?(has_more)
|
|
126
166
|
|
|
127
|
-
[data, has_more]
|
|
167
|
+
[payload, data, has_more]
|
|
128
168
|
end
|
|
129
169
|
|
|
130
170
|
def count(value)
|
|
@@ -138,7 +178,10 @@ module Volcano
|
|
|
138
178
|
end
|
|
139
179
|
|
|
140
180
|
def parse_time(value)
|
|
141
|
-
|
|
181
|
+
return Time.iso8601(value) if value.is_a?(String)
|
|
182
|
+
return value if value.nil? || value.is_a?(Time)
|
|
183
|
+
|
|
184
|
+
raise TypeError, INCOMPLETE_EXECUTION
|
|
142
185
|
end
|
|
143
186
|
end
|
|
144
187
|
private_constant :DurableResponses
|
|
@@ -32,7 +32,7 @@ module Volcano
|
|
|
32
32
|
|
|
33
33
|
def refresh(original)
|
|
34
34
|
# Resolution has released its cache lock before refresh callbacks run.
|
|
35
|
-
@client.auth.__send__(:refresh_captured_session,
|
|
35
|
+
@client.auth.__send__(:refresh_captured_session, active_binding)
|
|
36
36
|
rescue Error::SessionChangedError
|
|
37
37
|
raise
|
|
38
38
|
rescue Error::VolcanoError
|
|
@@ -41,11 +41,18 @@ module Volcano
|
|
|
41
41
|
|
|
42
42
|
def validate
|
|
43
43
|
if @binding.last
|
|
44
|
-
@client.auth.__send__(:validate_read_failure,
|
|
44
|
+
@client.auth.__send__(:validate_read_failure, active_binding)
|
|
45
45
|
elsif @client.capture_session_binding[1] != @binding[1]
|
|
46
46
|
raise Error::SessionChangedError
|
|
47
47
|
end
|
|
48
48
|
end
|
|
49
|
+
|
|
50
|
+
def active_binding
|
|
51
|
+
generation, owner, session = @binding
|
|
52
|
+
raise Error::SessionChangedError unless session
|
|
53
|
+
|
|
54
|
+
[generation, owner, session]
|
|
55
|
+
end
|
|
49
56
|
end
|
|
50
57
|
private_constant :FunctionAuth
|
|
51
58
|
end
|
data/lib/volcano/functions.rb
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
require_relative 'function_auth'
|
|
4
4
|
|
|
5
5
|
module Volcano
|
|
6
|
+
INVALID_FUNCTION_NAME = 'Function name must be DNS-safe: lowercase letters, numbers, and hyphens; 1-63 characters'
|
|
7
|
+
private_constant :INVALID_FUNCTION_NAME
|
|
8
|
+
|
|
6
9
|
# Invokes deployed Volcano functions by name.
|
|
7
10
|
class Functions
|
|
8
11
|
FUNCTION_NAME = /\A[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\z/
|
|
@@ -21,7 +24,7 @@ module Volcano
|
|
|
21
24
|
def invoke(name, payload = {})
|
|
22
25
|
auth = FunctionAuth.new(@client)
|
|
23
26
|
validate_invocation(name, payload)
|
|
24
|
-
invoke_owned(auth, name.dup.freeze,
|
|
27
|
+
invoke_owned(auth, name.dup.freeze, ImmutableRequestValue.capture(payload))
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
@@ -53,9 +56,7 @@ module Volcano
|
|
|
53
56
|
# X-Volcano-Function-Invoked only after dispatch, so its absence is what
|
|
54
57
|
# separates the two. X-Volcano-Version cannot: the server stamps it on every
|
|
55
58
|
# response, including errors raised before the function is reached.
|
|
56
|
-
def stale_mapping?(response)
|
|
57
|
-
response.status == 404 && header(response.headers, FUNCTION_INVOKED_HEADER).nil?
|
|
58
|
-
end
|
|
59
|
+
def stale_mapping?(response) = response.status == 404 && header(response.headers, FUNCTION_INVOKED_HEADER).nil?
|
|
59
60
|
|
|
60
61
|
def invoke_resolved(authorization, resolution, payload)
|
|
61
62
|
if resolution.invoke_url.nil?
|
|
@@ -77,8 +78,8 @@ module Volcano
|
|
|
77
78
|
# Hold the name's lock across the round trip so concurrent callers wait
|
|
78
79
|
# for one resolve instead of each opening their own.
|
|
79
80
|
FunctionResolution.resolve_lock(@api_url, authorization, name).synchronize do
|
|
80
|
-
|
|
81
|
-
next cached_resolution(
|
|
81
|
+
cached_under_lock = FunctionResolution.lookup(@api_url, authorization, name)
|
|
82
|
+
next cached_resolution(cached_under_lock) if cached_under_lock
|
|
82
83
|
|
|
83
84
|
resolve_uncached(authorization, name)
|
|
84
85
|
end
|
|
@@ -88,7 +89,7 @@ module Volcano
|
|
|
88
89
|
resolved = Transport.invoke do
|
|
89
90
|
@transport.resolve_function_for_invocation(authorization: authorization, name: name)
|
|
90
91
|
end
|
|
91
|
-
payload = Transport.body(resolved, 200)
|
|
92
|
+
payload = Transport.json_object(Transport.body(resolved, 200), message: 'Expected a complete function response')
|
|
92
93
|
resolution = resolved_resolution(payload)
|
|
93
94
|
FunctionResolution.store(@api_url, authorization, name, resolution, cache_ttl(payload))
|
|
94
95
|
resolution
|
|
@@ -98,24 +99,26 @@ module Volcano
|
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
def cached_resolution(cached)
|
|
101
|
-
cached.resolution
|
|
102
|
+
return cached.resolution if cached.resolution
|
|
103
|
+
|
|
104
|
+
failure = cached.failure || raise(TypeError, 'Expected a cached function resolution')
|
|
105
|
+
raise FunctionResolution.error_for(failure)
|
|
102
106
|
end
|
|
103
107
|
|
|
104
108
|
def validate_invocation(name, payload)
|
|
105
|
-
unless name.is_a?(String) && FUNCTION_NAME.match?(name)
|
|
106
|
-
raise ArgumentError,
|
|
107
|
-
'Function name must be DNS-safe: lowercase letters, numbers, and hyphens; 1-63 characters'
|
|
108
|
-
end
|
|
109
|
+
raise ArgumentError, INVALID_FUNCTION_NAME unless name.is_a?(String) && FUNCTION_NAME.match?(name)
|
|
109
110
|
raise TypeError, 'Function payload must be a Hash' unless payload.is_a?(Hash)
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def resolved_resolution(payload)
|
|
113
|
-
|
|
114
|
+
payload = Transport.json_object(payload)
|
|
115
|
+
function_id = payload['function_id']
|
|
114
116
|
raise TypeError, 'Expected a complete function response' unless function_id.is_a?(String) && !function_id.empty?
|
|
115
117
|
|
|
116
118
|
# Absent when the deployment serves no public invocation domain, as in
|
|
117
119
|
# local development; the function is reached through the API instead.
|
|
118
|
-
|
|
120
|
+
candidate = payload['invoke_url']
|
|
121
|
+
invoke_url = FunctionResolution.valid_invoke_url(candidate.is_a?(String) ? candidate : nil, @api_url)
|
|
119
122
|
FunctionResolution::Resolution.new(function_id: function_id, invoke_url: invoke_url)
|
|
120
123
|
end
|
|
121
124
|
|
|
@@ -136,7 +139,7 @@ module Volcano
|
|
|
136
139
|
Transport.body(response, 200) unless response.status.between?(200, 299) || dispatched
|
|
137
140
|
|
|
138
141
|
FunctionResponse.new(
|
|
139
|
-
data: response.body, status: response.status,
|
|
142
|
+
data: Transport.json_value(response.body), status: response.status,
|
|
140
143
|
headers: response.headers || {}, version: version
|
|
141
144
|
)
|
|
142
145
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby SDK runtime and immutable caller-owned request values.
|
|
4
|
+
module Volcano
|
|
5
|
+
# Copies JSON-like request values before storing them in builders or sending them.
|
|
6
|
+
module ImmutableRequestValue
|
|
7
|
+
def self.capture(value)
|
|
8
|
+
case value
|
|
9
|
+
when String, Time then value.dup.freeze
|
|
10
|
+
when Array then value.map { |item| capture(item) }.freeze
|
|
11
|
+
when Hash then capture_hash(value)
|
|
12
|
+
else value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.capture_hash(value)
|
|
17
|
+
value.to_h { |key, item| [capture(key), capture(item)] }.freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
private_constant :ImmutableRequestValue
|
|
21
|
+
end
|
data/lib/volcano/logs.rb
CHANGED
|
@@ -3,8 +3,6 @@
|
|
|
3
3
|
module Volcano
|
|
4
4
|
# Searches retained project logs and activity.
|
|
5
5
|
class Logs
|
|
6
|
-
include ImmutableLogJSON
|
|
7
|
-
|
|
8
6
|
def initialize(client, transport)
|
|
9
7
|
@client = client
|
|
10
8
|
@transport = transport
|
|
@@ -46,26 +44,27 @@ module Volcano
|
|
|
46
44
|
end
|
|
47
45
|
raise TypeError, 'Log request must be a Hash' unless request.is_a?(Hash)
|
|
48
46
|
|
|
49
|
-
[project_id.dup.freeze,
|
|
47
|
+
[project_id.dup.freeze, ImmutableRequestValue.capture(request)]
|
|
50
48
|
end
|
|
51
49
|
|
|
52
50
|
def search_response(body)
|
|
53
|
-
data = response_data(body)
|
|
51
|
+
body, data = response_data(body)
|
|
54
52
|
limit = body['limit']
|
|
55
53
|
has_more = body['has_more']
|
|
56
54
|
next_cursor = body['next_cursor']
|
|
57
|
-
unless limit.is_a?(Integer) &&
|
|
58
|
-
(next_cursor.nil? || next_cursor.is_a?(String))
|
|
55
|
+
unless limit.is_a?(Integer) && boolean?(has_more) && (next_cursor.nil? || next_cursor.is_a?(String))
|
|
59
56
|
raise TypeError, 'Expected a complete log response'
|
|
60
57
|
end
|
|
61
58
|
|
|
59
|
+
has_more = has_more == true
|
|
60
|
+
|
|
62
61
|
LogSearchResponse.new(
|
|
63
62
|
data: data, limit: limit, has_more: has_more, next_cursor: next_cursor
|
|
64
63
|
)
|
|
65
64
|
end
|
|
66
65
|
|
|
67
66
|
def activity_response(body)
|
|
68
|
-
data = response_data(body)
|
|
67
|
+
body, data = response_data(body)
|
|
69
68
|
total = body['total']
|
|
70
69
|
raise TypeError, 'Expected a complete log response' unless total.is_a?(Integer)
|
|
71
70
|
|
|
@@ -73,10 +72,16 @@ module Volcano
|
|
|
73
72
|
end
|
|
74
73
|
|
|
75
74
|
def response_data(body)
|
|
76
|
-
|
|
75
|
+
raise TypeError, 'Expected a complete log response' unless body.is_a?(Hash)
|
|
76
|
+
|
|
77
|
+
data = body['data']
|
|
77
78
|
raise TypeError, 'Expected a complete log response' unless data.is_a?(Array) && data.all?(Hash)
|
|
78
79
|
|
|
79
|
-
data
|
|
80
|
+
[body, data]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def boolean?(value)
|
|
84
|
+
value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
80
85
|
end
|
|
81
86
|
end
|
|
82
87
|
end
|