forest_admin_datasource_rpc 1.17.0 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/forest_admin_datasource_rpc.gemspec +0 -1
- data/lib/forest_admin_datasource_rpc/Utils/rpc_client.rb +33 -2
- data/lib/forest_admin_datasource_rpc/Utils/schema_polling_client.rb +219 -0
- data/lib/forest_admin_datasource_rpc/datasource.rb +6 -6
- data/lib/forest_admin_datasource_rpc/version.rb +1 -1
- data/lib/forest_admin_datasource_rpc.rb +33 -19
- metadata +2 -16
- data/lib/forest_admin_datasource_rpc/Utils/sse_client.rb +0 -212
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9485d38ceffa876811869166eb3b74209f0c1a9807c4a2b3571b1271a552d428
|
|
4
|
+
data.tar.gz: 4dff025fcea11c17531f412b37ae13dfa68485484aa74fd7ccfcf5c3b11fc829
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d0212c1ab3221ddc96ab35dbe789e362108cdb2d48cad36c47b47920545f9d55df998dc55bf94f8f6779f2b8174ca2fcb0e40817d39e7c24f8337ecfc5c9f092
|
|
7
|
+
data.tar.gz: 20246dff2afeae14a3d2e8b7aed1dc64f5179b1254fb3bfe8f9ea3c5e585b312d82a85b04ccd6ba78ae4486a650be63fb53f12f65c6a83bdf9343f46682f8c3d
|
|
@@ -36,7 +36,6 @@ admin work on any Ruby application."
|
|
|
36
36
|
spec.add_dependency "bigdecimal"
|
|
37
37
|
spec.add_dependency "csv"
|
|
38
38
|
spec.add_dependency "faraday", "~> 2.7"
|
|
39
|
-
spec.add_dependency "ld-eventsource", "~> 2.2"
|
|
40
39
|
spec.add_dependency "mutex_m"
|
|
41
40
|
spec.add_dependency "ostruct"
|
|
42
41
|
spec.add_dependency "zeitwerk", "~> 2.3"
|
|
@@ -56,6 +56,8 @@ module ForestAdminDatasourceRpc
|
|
|
56
56
|
|
|
57
57
|
# rubocop:disable Metrics/ParameterLists
|
|
58
58
|
def make_request(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false, if_none_match: nil)
|
|
59
|
+
log_request_start(method, endpoint, if_none_match)
|
|
60
|
+
|
|
59
61
|
client = Faraday.new(url: @api_url) do |faraday|
|
|
60
62
|
faraday.request :json
|
|
61
63
|
faraday.response :json, parser_options: { symbolize_names: symbolize_keys }
|
|
@@ -75,7 +77,9 @@ module ForestAdminDatasourceRpc
|
|
|
75
77
|
headers['forest_caller'] = caller.to_json if caller
|
|
76
78
|
headers['If-None-Match'] = %("#{if_none_match}") if if_none_match
|
|
77
79
|
|
|
78
|
-
client.send(method, endpoint, payload, headers)
|
|
80
|
+
response = client.send(method, endpoint, payload, headers)
|
|
81
|
+
log_request_complete(response, endpoint)
|
|
82
|
+
response
|
|
79
83
|
end
|
|
80
84
|
# rubocop:enable Metrics/ParameterLists
|
|
81
85
|
|
|
@@ -91,7 +95,14 @@ module ForestAdminDatasourceRpc
|
|
|
91
95
|
end
|
|
92
96
|
|
|
93
97
|
def handle_response_with_etag(response)
|
|
94
|
-
|
|
98
|
+
if response.success?
|
|
99
|
+
etag = extract_etag(response)
|
|
100
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
101
|
+
'Debug',
|
|
102
|
+
"[RPC Client] Schema response received (status: #{response.status}, ETag: #{etag || "none"})"
|
|
103
|
+
)
|
|
104
|
+
return SchemaResponse.new(response.body, etag)
|
|
105
|
+
end
|
|
95
106
|
return NotModified if response.status == HTTP_NOT_MODIFIED
|
|
96
107
|
|
|
97
108
|
raise_appropriate_error(response)
|
|
@@ -108,6 +119,11 @@ module ForestAdminDatasourceRpc
|
|
|
108
119
|
url = response.env.url
|
|
109
120
|
message = error_body[:message] || generate_default_message(status, url)
|
|
110
121
|
|
|
122
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
123
|
+
'Error',
|
|
124
|
+
"[RPC Client] Request failed (status: #{status}, URL: #{url}, message: #{message})"
|
|
125
|
+
)
|
|
126
|
+
|
|
111
127
|
exception_class = ERROR_STATUS_MAP[status]
|
|
112
128
|
|
|
113
129
|
if exception_class
|
|
@@ -143,6 +159,21 @@ module ForestAdminDatasourceRpc
|
|
|
143
159
|
name: hash['name'] || hash[:name]
|
|
144
160
|
}.compact
|
|
145
161
|
end
|
|
162
|
+
|
|
163
|
+
def log_request_start(method, endpoint, if_none_match)
|
|
164
|
+
etag_info = if_none_match ? " (If-None-Match: #{if_none_match})" : ''
|
|
165
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
166
|
+
'Debug',
|
|
167
|
+
"[RPC Client] Sending #{method.to_s.upcase} request to #{@api_url}#{endpoint}#{etag_info}"
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def log_request_complete(response, endpoint)
|
|
172
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
173
|
+
'Debug',
|
|
174
|
+
"[RPC Client] Response received (status: #{response.status}, endpoint: #{endpoint})"
|
|
175
|
+
)
|
|
176
|
+
end
|
|
146
177
|
end
|
|
147
178
|
end
|
|
148
179
|
end
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
require 'openssl'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module ForestAdminDatasourceRpc
|
|
6
|
+
module Utils
|
|
7
|
+
class SchemaPollingClient
|
|
8
|
+
attr_reader :closed
|
|
9
|
+
|
|
10
|
+
DEFAULT_POLLING_INTERVAL = 600 # seconds (10 minutes)
|
|
11
|
+
MIN_POLLING_INTERVAL = 1 # seconds (minimum safe interval)
|
|
12
|
+
MAX_POLLING_INTERVAL = 3600 # seconds (1 hour max)
|
|
13
|
+
|
|
14
|
+
def initialize(uri, auth_secret, options = {}, &on_schema_change)
|
|
15
|
+
@uri = uri
|
|
16
|
+
@auth_secret = auth_secret
|
|
17
|
+
@polling_interval = options[:polling_interval] || DEFAULT_POLLING_INTERVAL
|
|
18
|
+
@on_schema_change = on_schema_change
|
|
19
|
+
@closed = false
|
|
20
|
+
@cached_etag = nil
|
|
21
|
+
@polling_thread = nil
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
@connection_attempts = 0
|
|
24
|
+
|
|
25
|
+
# Validate polling interval
|
|
26
|
+
validate_polling_interval!
|
|
27
|
+
|
|
28
|
+
# RPC client for schema fetching with ETag support
|
|
29
|
+
@rpc_client = RpcClient.new(@uri, @auth_secret)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start
|
|
33
|
+
return if @closed
|
|
34
|
+
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
return if @polling_thread&.alive?
|
|
37
|
+
|
|
38
|
+
@polling_thread = Thread.new do
|
|
39
|
+
polling_loop
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
42
|
+
'Error',
|
|
43
|
+
"[Schema Polling] Unexpected error in polling loop: #{e.class} - #{e.message}"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
49
|
+
'Info',
|
|
50
|
+
"[Schema Polling] Polling started (interval: #{@polling_interval}s)"
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def stop
|
|
55
|
+
return if @closed
|
|
56
|
+
|
|
57
|
+
@closed = true
|
|
58
|
+
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[Schema Polling] Stopping polling')
|
|
59
|
+
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
if @polling_thread&.alive?
|
|
62
|
+
@polling_thread.kill
|
|
63
|
+
@polling_thread = nil
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[Schema Polling] Polling stopped')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def polling_loop
|
|
73
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
74
|
+
'Debug',
|
|
75
|
+
"[Schema Polling] Starting polling loop (interval: #{@polling_interval}s)"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
loop do
|
|
79
|
+
break if @closed
|
|
80
|
+
|
|
81
|
+
begin
|
|
82
|
+
check_schema
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
handle_error(e)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Sleep with interrupt check (check every second for early termination)
|
|
88
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
89
|
+
'Debug',
|
|
90
|
+
"[Schema Polling] Waiting #{@polling_interval}s before next check (current ETag: #{@cached_etag || "none"})"
|
|
91
|
+
)
|
|
92
|
+
remaining = @polling_interval
|
|
93
|
+
while remaining.positive? && !@closed
|
|
94
|
+
sleep([remaining, 1].min)
|
|
95
|
+
remaining -= 1
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def check_schema
|
|
101
|
+
@connection_attempts += 1
|
|
102
|
+
log_checking_schema
|
|
103
|
+
|
|
104
|
+
result = @rpc_client.fetch_schema('/forest/rpc-schema', if_none_match: @cached_etag)
|
|
105
|
+
handle_schema_result(result)
|
|
106
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError => e
|
|
107
|
+
log_connection_error(e)
|
|
108
|
+
rescue ForestAdminAgent::Http::Exceptions::AuthenticationOpenIdClient => e
|
|
109
|
+
log_authentication_error(e)
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
log_unexpected_error(e)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def handle_error(error)
|
|
115
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
116
|
+
'Error',
|
|
117
|
+
"[Schema Polling] Error during schema check: #{error.class} - #{error.message}"
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def trigger_schema_change_callback(schema)
|
|
122
|
+
return unless @on_schema_change
|
|
123
|
+
|
|
124
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
125
|
+
'Debug',
|
|
126
|
+
'[Schema Polling] Invoking schema change callback'
|
|
127
|
+
)
|
|
128
|
+
begin
|
|
129
|
+
@on_schema_change.call(schema)
|
|
130
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
131
|
+
'Debug',
|
|
132
|
+
'[Schema Polling] Schema change callback completed successfully'
|
|
133
|
+
)
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
error_msg = "[Schema Polling] Error in schema change callback: #{e.class} - #{e.message}"
|
|
136
|
+
backtrace = "\n#{e.backtrace&.first(5)&.join("\n")}"
|
|
137
|
+
ForestAdminAgent::Facades::Container.logger&.log('Error', error_msg + backtrace)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def log_checking_schema
|
|
142
|
+
etag_info = @cached_etag ? "with ETag: #{@cached_etag}" : 'without ETag (initial fetch)'
|
|
143
|
+
msg = "[Schema Polling] Checking schema from #{@uri}/forest/rpc-schema " \
|
|
144
|
+
"(attempt ##{@connection_attempts}, #{etag_info})"
|
|
145
|
+
ForestAdminAgent::Facades::Container.logger&.log('Debug', msg)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def handle_schema_result(result)
|
|
149
|
+
if result == RpcClient::NotModified
|
|
150
|
+
handle_schema_unchanged
|
|
151
|
+
else
|
|
152
|
+
handle_schema_changed(result)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def handle_schema_unchanged
|
|
157
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
158
|
+
'Debug',
|
|
159
|
+
"[Schema Polling] Schema unchanged (HTTP 304 Not Modified), ETag still valid: #{@cached_etag}"
|
|
160
|
+
)
|
|
161
|
+
@connection_attempts = 0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def handle_schema_changed(result)
|
|
165
|
+
new_etag = result.etag
|
|
166
|
+
@cached_etag.nil? ? handle_initial_schema(new_etag) : handle_schema_update(result.body, new_etag)
|
|
167
|
+
@connection_attempts = 0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def handle_initial_schema(etag)
|
|
171
|
+
@cached_etag = etag
|
|
172
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
173
|
+
'Debug',
|
|
174
|
+
"[Schema Polling] Initial schema loaded successfully (ETag: #{etag})"
|
|
175
|
+
)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def handle_schema_update(schema, etag)
|
|
179
|
+
old_etag = @cached_etag
|
|
180
|
+
@cached_etag = etag
|
|
181
|
+
msg = "[Schema Polling] Schema changed detected (old ETag: #{old_etag}, new ETag: #{etag}), " \
|
|
182
|
+
'triggering reload callback'
|
|
183
|
+
ForestAdminAgent::Facades::Container.logger&.log('Info', msg)
|
|
184
|
+
trigger_schema_change_callback(schema)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def log_connection_error(error)
|
|
188
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
189
|
+
'Warn',
|
|
190
|
+
"[Schema Polling] Connection error: #{error.class} - #{error.message}"
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def log_authentication_error(error)
|
|
195
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
196
|
+
'Error',
|
|
197
|
+
"[Schema Polling] Authentication error: #{error.message}"
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def log_unexpected_error(error)
|
|
202
|
+
ForestAdminAgent::Facades::Container.logger&.log(
|
|
203
|
+
'Error',
|
|
204
|
+
"[Schema Polling] Unexpected error: #{error.class} - #{error.message}"
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def validate_polling_interval!
|
|
209
|
+
if @polling_interval < MIN_POLLING_INTERVAL
|
|
210
|
+
raise ArgumentError,
|
|
211
|
+
"Schema polling interval too short: #{@polling_interval}s (minimum: #{MIN_POLLING_INTERVAL}s)"
|
|
212
|
+
elsif @polling_interval > MAX_POLLING_INTERVAL
|
|
213
|
+
raise ArgumentError,
|
|
214
|
+
"Schema polling interval too long: #{@polling_interval}s (maximum: #{MAX_POLLING_INTERVAL}s)"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
@@ -2,7 +2,7 @@ module ForestAdminDatasourceRpc
|
|
|
2
2
|
class Datasource < ForestAdminDatasourceToolkit::Datasource
|
|
3
3
|
include ForestAdminDatasourceRpc::Utils
|
|
4
4
|
|
|
5
|
-
def initialize(options, introspection,
|
|
5
|
+
def initialize(options, introspection, schema_polling_client = nil)
|
|
6
6
|
super()
|
|
7
7
|
|
|
8
8
|
ForestAdminAgent::Facades::Container.logger.log(
|
|
@@ -18,7 +18,7 @@ module ForestAdminDatasourceRpc
|
|
|
18
18
|
@options = options
|
|
19
19
|
@charts = introspection[:charts]
|
|
20
20
|
@rpc_relations = introspection[:rpc_relations]
|
|
21
|
-
@
|
|
21
|
+
@schema_polling_client = schema_polling_client
|
|
22
22
|
@cleaned_up = false
|
|
23
23
|
|
|
24
24
|
native_query_connections = introspection[:native_query_connections] || []
|
|
@@ -58,10 +58,10 @@ module ForestAdminDatasourceRpc
|
|
|
58
58
|
|
|
59
59
|
@cleaned_up = true
|
|
60
60
|
|
|
61
|
-
if @
|
|
62
|
-
ForestAdminAgent::Facades::Container.logger&.log('Info', '[RPCDatasource]
|
|
63
|
-
@
|
|
64
|
-
ForestAdminAgent::Facades::Container.logger&.log('Info', '[RPCDatasource]
|
|
61
|
+
if @schema_polling_client
|
|
62
|
+
ForestAdminAgent::Facades::Container.logger&.log('Info', '[RPCDatasource] Stopping schema polling...')
|
|
63
|
+
@schema_polling_client.stop
|
|
64
|
+
ForestAdminAgent::Facades::Container.logger&.log('Info', '[RPCDatasource] Schema polling stopped')
|
|
65
65
|
end
|
|
66
66
|
rescue StandardError => e
|
|
67
67
|
ForestAdminAgent::Facades::Container.logger&.log(
|
|
@@ -8,20 +8,30 @@ loader.setup
|
|
|
8
8
|
module ForestAdminDatasourceRpc
|
|
9
9
|
class Error < StandardError; end
|
|
10
10
|
|
|
11
|
+
# Build a RPC datasource with schema polling enabled.
|
|
12
|
+
#
|
|
13
|
+
# @param options [Hash] Configuration options
|
|
14
|
+
# @option options [String] :uri The URI of the RPC agent
|
|
15
|
+
# @option options [String] :auth_secret The authentication secret (optional, will use cache if not provided)
|
|
16
|
+
# @option options [Integer] :schema_polling_interval Polling interval in seconds (optional)
|
|
17
|
+
# - Default: 600 seconds (10 minutes)
|
|
18
|
+
# - Can be overridden with ENV['SCHEMA_POLLING_INTERVAL']
|
|
19
|
+
# - Valid range: 1-3600 seconds
|
|
20
|
+
# - Priority: options[:schema_polling_interval] > ENV['SCHEMA_POLLING_INTERVAL'] > default
|
|
21
|
+
# - Example: SCHEMA_POLLING_INTERVAL=30 for development (30 seconds)
|
|
22
|
+
#
|
|
23
|
+
# @return [ForestAdminDatasourceRpc::Datasource] The configured datasource with schema polling
|
|
11
24
|
def self.build(options)
|
|
12
25
|
uri = options[:uri]
|
|
13
26
|
auth_secret = options[:auth_secret] || ForestAdminAgent::Facades::Container.cache(:auth_secret)
|
|
14
27
|
ForestAdminAgent::Facades::Container.logger.log('Info', "Getting schema from RPC agent on #{uri}.")
|
|
15
28
|
|
|
16
29
|
schema = nil
|
|
17
|
-
cached_etag = nil
|
|
18
30
|
|
|
19
31
|
begin
|
|
20
32
|
rpc_client = Utils::RpcClient.new(uri, auth_secret)
|
|
21
33
|
response = rpc_client.fetch_schema('/forest/rpc-schema')
|
|
22
34
|
schema = response.body
|
|
23
|
-
# Use the ETag header for conditional requests
|
|
24
|
-
cached_etag = response.etag
|
|
25
35
|
rescue Faraday::ConnectionFailed => e
|
|
26
36
|
ForestAdminAgent::Facades::Container.logger.log(
|
|
27
37
|
'Error',
|
|
@@ -48,27 +58,31 @@ module ForestAdminDatasourceRpc
|
|
|
48
58
|
# return empty datasource for not breaking stack
|
|
49
59
|
ForestAdminDatasourceToolkit::Datasource.new
|
|
50
60
|
else
|
|
51
|
-
|
|
52
|
-
|
|
61
|
+
# Create schema polling client with configurable polling interval
|
|
62
|
+
# Priority: options[:schema_polling_interval] > ENV['SCHEMA_POLLING_INTERVAL'] > default (600)
|
|
63
|
+
polling_interval = if options[:schema_polling_interval]
|
|
64
|
+
options[:schema_polling_interval]
|
|
65
|
+
elsif ENV['SCHEMA_POLLING_INTERVAL']
|
|
66
|
+
ENV['SCHEMA_POLLING_INTERVAL'].to_i
|
|
67
|
+
else
|
|
68
|
+
600 # 10 minutes by default
|
|
69
|
+
end
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
polling_options = {
|
|
72
|
+
polling_interval: polling_interval
|
|
73
|
+
}
|
|
56
74
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
cached_etag = result.etag
|
|
63
|
-
ForestAdminAgent::Facades::Container.logger.log('Info', '[RPCDatasource] Schema changed, reloading agent...')
|
|
64
|
-
ForestAdminAgent::Builder::AgentFactory.instance.reload!
|
|
65
|
-
end
|
|
75
|
+
schema_polling = Utils::SchemaPollingClient.new(uri, auth_secret, polling_options) do
|
|
76
|
+
# Callback when schema change is detected
|
|
77
|
+
logger = ForestAdminAgent::Facades::Container.logger
|
|
78
|
+
logger.log('Info', '[RPCDatasource] Schema change detected, reloading agent...')
|
|
79
|
+
ForestAdminAgent::Builder::AgentFactory.instance.reload!
|
|
66
80
|
end
|
|
67
|
-
|
|
81
|
+
schema_polling.start
|
|
68
82
|
|
|
69
|
-
datasource = ForestAdminDatasourceRpc::Datasource.new(options, schema,
|
|
83
|
+
datasource = ForestAdminDatasourceRpc::Datasource.new(options, schema, schema_polling)
|
|
70
84
|
|
|
71
|
-
# Setup cleanup hooks for proper
|
|
85
|
+
# Setup cleanup hooks for proper schema polling client shutdown
|
|
72
86
|
setup_cleanup_hooks(datasource)
|
|
73
87
|
|
|
74
88
|
datasource
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_admin_datasource_rpc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthieu
|
|
@@ -67,20 +67,6 @@ dependencies:
|
|
|
67
67
|
- - "~>"
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
69
|
version: '2.7'
|
|
70
|
-
- !ruby/object:Gem::Dependency
|
|
71
|
-
name: ld-eventsource
|
|
72
|
-
requirement: !ruby/object:Gem::Requirement
|
|
73
|
-
requirements:
|
|
74
|
-
- - "~>"
|
|
75
|
-
- !ruby/object:Gem::Version
|
|
76
|
-
version: '2.2'
|
|
77
|
-
type: :runtime
|
|
78
|
-
prerelease: false
|
|
79
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
80
|
-
requirements:
|
|
81
|
-
- - "~>"
|
|
82
|
-
- !ruby/object:Gem::Version
|
|
83
|
-
version: '2.2'
|
|
84
70
|
- !ruby/object:Gem::Dependency
|
|
85
71
|
name: mutex_m
|
|
86
72
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -139,7 +125,7 @@ files:
|
|
|
139
125
|
- forest_admin_datasource_rpc.gemspec
|
|
140
126
|
- lib/forest_admin_datasource_rpc.rb
|
|
141
127
|
- lib/forest_admin_datasource_rpc/Utils/rpc_client.rb
|
|
142
|
-
- lib/forest_admin_datasource_rpc/Utils/
|
|
128
|
+
- lib/forest_admin_datasource_rpc/Utils/schema_polling_client.rb
|
|
143
129
|
- lib/forest_admin_datasource_rpc/collection.rb
|
|
144
130
|
- lib/forest_admin_datasource_rpc/datasource.rb
|
|
145
131
|
- lib/forest_admin_datasource_rpc/version.rb
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
require 'openssl'
|
|
2
|
-
require 'json'
|
|
3
|
-
require 'time'
|
|
4
|
-
require 'ld-eventsource'
|
|
5
|
-
|
|
6
|
-
module ForestAdminDatasourceRpc
|
|
7
|
-
module Utils
|
|
8
|
-
class SseClient
|
|
9
|
-
attr_reader :closed
|
|
10
|
-
|
|
11
|
-
MAX_BACKOFF_DELAY = 30 # seconds
|
|
12
|
-
INITIAL_BACKOFF_DELAY = 2 # seconds
|
|
13
|
-
|
|
14
|
-
def initialize(uri, auth_secret, &on_rpc_stop)
|
|
15
|
-
@uri = uri
|
|
16
|
-
@auth_secret = auth_secret
|
|
17
|
-
@on_rpc_stop = on_rpc_stop
|
|
18
|
-
@client = nil
|
|
19
|
-
@closed = false
|
|
20
|
-
@connection_attempts = 0
|
|
21
|
-
@reconnect_thread = nil
|
|
22
|
-
@connecting = false
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def start
|
|
26
|
-
return if @closed
|
|
27
|
-
|
|
28
|
-
attempt_connection
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def close
|
|
32
|
-
return if @closed
|
|
33
|
-
|
|
34
|
-
@closed = true
|
|
35
|
-
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Closing connection')
|
|
36
|
-
|
|
37
|
-
# Stop reconnection thread if running
|
|
38
|
-
if @reconnect_thread&.alive?
|
|
39
|
-
@reconnect_thread.kill
|
|
40
|
-
@reconnect_thread = nil
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
begin
|
|
44
|
-
@client&.close
|
|
45
|
-
rescue StandardError => e
|
|
46
|
-
ForestAdminAgent::Facades::Container.logger&.log('Debug',
|
|
47
|
-
"[SSE Client] Error during close: #{e.message}")
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Connection closed')
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def attempt_connection
|
|
56
|
-
return if @closed
|
|
57
|
-
return if @connecting
|
|
58
|
-
|
|
59
|
-
@connecting = true
|
|
60
|
-
@connection_attempts += 1
|
|
61
|
-
timestamp = Time.now.utc.iso8601(3)
|
|
62
|
-
signature = generate_signature(timestamp)
|
|
63
|
-
|
|
64
|
-
headers = {
|
|
65
|
-
'Accept' => 'text/event-stream',
|
|
66
|
-
'X_TIMESTAMP' => timestamp,
|
|
67
|
-
'X_SIGNATURE' => signature
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
ForestAdminAgent::Facades::Container.logger&.log(
|
|
71
|
-
'Debug',
|
|
72
|
-
"[SSE Client] Connecting to #{@uri} (attempt ##{@connection_attempts})"
|
|
73
|
-
)
|
|
74
|
-
|
|
75
|
-
begin
|
|
76
|
-
# Close existing client if any
|
|
77
|
-
begin
|
|
78
|
-
@client&.close
|
|
79
|
-
rescue StandardError
|
|
80
|
-
# Ignore close errors
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
@client = SSE::Client.new(@uri, headers: headers) do |client|
|
|
84
|
-
client.on_event do |event|
|
|
85
|
-
handle_event(event)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
client.on_error do |err|
|
|
89
|
-
handle_error_with_reconnect(err)
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Connected successfully')
|
|
94
|
-
rescue StandardError => e
|
|
95
|
-
ForestAdminAgent::Facades::Container.logger&.log(
|
|
96
|
-
'Error',
|
|
97
|
-
"[SSE Client] Failed to connect: #{e.class} - #{e.message}"
|
|
98
|
-
)
|
|
99
|
-
@connecting = false
|
|
100
|
-
schedule_reconnect
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def handle_error_with_reconnect(err)
|
|
105
|
-
# Ignore errors when client is intentionally closed
|
|
106
|
-
return if @closed
|
|
107
|
-
|
|
108
|
-
is_auth_error = false
|
|
109
|
-
log_level = 'Warn'
|
|
110
|
-
|
|
111
|
-
error_message = case err
|
|
112
|
-
when SSE::Errors::HTTPStatusError
|
|
113
|
-
# Extract more details from HTTP errors
|
|
114
|
-
status = err.respond_to?(:status) ? err.status : 'unknown'
|
|
115
|
-
body = err.respond_to?(:body) && !err.body.to_s.strip.empty? ? err.body : 'empty response'
|
|
116
|
-
is_auth_error = status.to_s =~ /^(401|403)$/
|
|
117
|
-
|
|
118
|
-
# Auth errors during reconnection are expected (server shutdown or credentials expiring)
|
|
119
|
-
log_level = 'Debug' if is_auth_error
|
|
120
|
-
|
|
121
|
-
"HTTP #{status} - #{body}"
|
|
122
|
-
when EOFError, IOError
|
|
123
|
-
# Connection lost is expected when server stops
|
|
124
|
-
log_level = 'Debug'
|
|
125
|
-
"Connection lost: #{err.class}"
|
|
126
|
-
when StandardError
|
|
127
|
-
"#{err.class} - #{err.message}"
|
|
128
|
-
else
|
|
129
|
-
err.to_s
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
ForestAdminAgent::Facades::Container.logger&.log(log_level, "[SSE Client] Error: #{error_message}")
|
|
133
|
-
|
|
134
|
-
# Close client immediately to prevent ld-eventsource from reconnecting with stale credentials
|
|
135
|
-
begin
|
|
136
|
-
@client&.close
|
|
137
|
-
rescue StandardError
|
|
138
|
-
# Ignore close errors
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Reset connecting flag and schedule reconnection
|
|
142
|
-
@connecting = false
|
|
143
|
-
|
|
144
|
-
# For auth errors, increase attempt count to get longer backoff
|
|
145
|
-
@connection_attempts += 2 if is_auth_error
|
|
146
|
-
|
|
147
|
-
schedule_reconnect
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def schedule_reconnect
|
|
151
|
-
return if @closed
|
|
152
|
-
return if @reconnect_thread&.alive?
|
|
153
|
-
|
|
154
|
-
@reconnect_thread = Thread.new do
|
|
155
|
-
delay = calculate_backoff_delay
|
|
156
|
-
ForestAdminAgent::Facades::Container.logger&.log(
|
|
157
|
-
'Debug',
|
|
158
|
-
"[SSE Client] Reconnecting in #{delay} seconds..."
|
|
159
|
-
)
|
|
160
|
-
sleep(delay)
|
|
161
|
-
attempt_connection unless @closed
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def calculate_backoff_delay
|
|
166
|
-
# Exponential backoff: 1, 2, 4, 8, 16, 30, 30, ...
|
|
167
|
-
delay = INITIAL_BACKOFF_DELAY * (2**[@connection_attempts - 1, 0].max)
|
|
168
|
-
[delay, MAX_BACKOFF_DELAY].min
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def handle_event(event)
|
|
172
|
-
type = event.type.to_s.strip
|
|
173
|
-
data = event.data.to_s.strip
|
|
174
|
-
|
|
175
|
-
case type
|
|
176
|
-
when 'heartbeat'
|
|
177
|
-
if @connecting
|
|
178
|
-
@connecting = false
|
|
179
|
-
@connection_attempts = 0
|
|
180
|
-
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[SSE Client] Connection stable')
|
|
181
|
-
end
|
|
182
|
-
when 'RpcServerStop'
|
|
183
|
-
ForestAdminAgent::Facades::Container.logger&.log('Debug', '[SSE Client] RpcServerStop received')
|
|
184
|
-
handle_rpc_stop
|
|
185
|
-
else
|
|
186
|
-
ForestAdminAgent::Facades::Container.logger&.log(
|
|
187
|
-
'Debug',
|
|
188
|
-
"[SSE Client] Unknown event: #{type} with payload: #{data}"
|
|
189
|
-
)
|
|
190
|
-
end
|
|
191
|
-
rescue StandardError => e
|
|
192
|
-
ForestAdminAgent::Facades::Container.logger&.log(
|
|
193
|
-
'Error',
|
|
194
|
-
"[SSE Client] Error handling event: #{e.class} - #{e.message}"
|
|
195
|
-
)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
def handle_rpc_stop
|
|
199
|
-
@on_rpc_stop&.call
|
|
200
|
-
rescue StandardError => e
|
|
201
|
-
ForestAdminAgent::Facades::Container.logger&.log(
|
|
202
|
-
'Error',
|
|
203
|
-
"[SSE Client] Error in RPC stop callback: #{e.class} - #{e.message}"
|
|
204
|
-
)
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def generate_signature(timestamp)
|
|
208
|
-
OpenSSL::HMAC.hexdigest('SHA256', @auth_secret, timestamp)
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|