hyperprobe-agent 1.2.27.pre.3-java
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 +7 -0
- data/LICENSE +13 -0
- data/README.md +386 -0
- data/lib/hyperprobe/agent.rb +537 -0
- data/lib/hyperprobe/core/broker.rb +183 -0
- data/lib/hyperprobe/core/evaluator.rb +418 -0
- data/lib/hyperprobe/core/lexical_scope.rb +222 -0
- data/lib/hyperprobe/core/logger.rb +198 -0
- data/lib/hyperprobe/core/monitoring_engine.rb +857 -0
- data/lib/hyperprobe/core/quota.rb +111 -0
- data/lib/hyperprobe/core/safe_ast_validator.rb +170 -0
- data/lib/hyperprobe/core/safety.rb +256 -0
- data/lib/hyperprobe/core/serializer.rb +437 -0
- data/lib/hyperprobe/core/trace_extractor.rb +158 -0
- data/lib/hyperprobe/core/transports/java_grpc.rb +57 -0
- data/lib/hyperprobe/jars/hyperprobe-grpc.jar +0 -0
- data/lib/hyperprobe/lambda.rb +92 -0
- data/lib/hyperprobe/protos/agent_descriptor.rb +7 -0
- data/lib/hyperprobe/protos/agent_pb.rb +33 -0
- data/lib/hyperprobe/protos/agent_services_pb.rb +31 -0
- data/lib/hyperprobe/protos/java_messages.rb +199 -0
- data/lib/hyperprobe/protos.rb +14 -0
- data/lib/hyperprobe/railtie.rb +22 -0
- data/lib/hyperprobe/version.rb +5 -0
- data/lib/hyperprobe-agent.rb +3 -0
- data/lib/hyperprobe.rb +282 -0
- data/transport/README.md +98 -0
- data/transport/THIRD_PARTY_NOTICES.md +57 -0
- data/transport/pom.xml +78 -0
- data/transport/src/main/java/co/hyperprobe/transport/GrpcTransport.java +118 -0
- data/transport/src/main/java/co/hyperprobe/transport/ProtoCodec.java +43 -0
- metadata +117 -0
data/lib/hyperprobe.rb
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require_relative 'hyperprobe/version'
|
|
5
|
+
require_relative 'hyperprobe/protos'
|
|
6
|
+
require_relative 'hyperprobe/core/logger'
|
|
7
|
+
require_relative 'hyperprobe/agent'
|
|
8
|
+
require_relative 'hyperprobe/lambda'
|
|
9
|
+
require_relative 'hyperprobe/railtie' if defined?(::Rails::Railtie)
|
|
10
|
+
rescue SignalException, SystemExit
|
|
11
|
+
raise
|
|
12
|
+
rescue Exception
|
|
13
|
+
# Missing native dependencies or a broken JVM transport disable only the SDK.
|
|
14
|
+
module HyperProbe
|
|
15
|
+
LOAD_FAILED = true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module HyperProbe
|
|
20
|
+
@instance = nil
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@owner_pid = Process.pid
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def logger(namespace = 'hyperprobe:agent')
|
|
26
|
+
Core::Logger.get_logger(namespace)
|
|
27
|
+
end
|
|
28
|
+
def start(options = {})
|
|
29
|
+
reset_after_fork
|
|
30
|
+
return nil if defined?(LOAD_FAILED) || @fork_disabled
|
|
31
|
+
return nil unless @mutex.try_lock
|
|
32
|
+
locked = true
|
|
33
|
+
begin
|
|
34
|
+
return @instance if @instance
|
|
35
|
+
@stop_requested = false
|
|
36
|
+
|
|
37
|
+
if ENV['HYPERPROBE_DISABLED']&.strip&.upcase == 'YES'
|
|
38
|
+
puts '[HyperProbe] Explicitly disabled via ENV["HYPERPROBE_DISABLED"].'
|
|
39
|
+
return nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts = (options || {}).dup
|
|
43
|
+
commit_sha = opts[:commit_sha] || opts[:commitSha] || opts['commit_sha'] || opts['commitSha'] || ENV['GIT_COMMIT'] || ENV['HYPERPROBE_COMMIT_SHA']
|
|
44
|
+
if commit_sha.nil? || commit_sha.to_s.strip.empty? || commit_sha.to_s.strip.downcase == 'unknown'
|
|
45
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. A valid \"commit_sha\" is required via options or the GIT_COMMIT environment variable to ensure accurate source map resolution and prevent cross-deployment collisions.\e[0m"
|
|
46
|
+
return nil
|
|
47
|
+
end
|
|
48
|
+
opts[:commit_sha] = commit_sha
|
|
49
|
+
|
|
50
|
+
service_id = opts[:service_id] || opts[:serviceId] || opts['service_id'] || opts['serviceId'] || ENV['HYPERPROBE_SERVICE_ID']
|
|
51
|
+
if service_id.nil? || service_id.to_s.strip.empty?
|
|
52
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. HYPERPROBE_SERVICE_ID is required.\e[0m"
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
if service_id && !UUID_REGEX.match?(service_id.to_s)
|
|
56
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] WARN: service_id is not a valid UUID, please check again.\e[0m"
|
|
57
|
+
end
|
|
58
|
+
opts[:service_id] = service_id
|
|
59
|
+
|
|
60
|
+
environment = opts[:environment] || opts['environment'] || ENV['HYPERPROBE_ENVIRONMENT']
|
|
61
|
+
if environment.nil? || environment.to_s.strip.empty?
|
|
62
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. HYPERPROBE_ENVIRONMENT is required.\e[0m"
|
|
63
|
+
return nil
|
|
64
|
+
end
|
|
65
|
+
opts[:environment] = environment
|
|
66
|
+
|
|
67
|
+
broker_url = opts[:broker_url] || opts[:brokerUrl] || opts['broker_url'] || opts['brokerUrl'] || ENV['HYPERPROBE_BROKER_URL']
|
|
68
|
+
unless valid_broker_url?(broker_url)
|
|
69
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] CRITICAL: Failed to start agent. Invalid broker_url \"#{broker_url}\". It must be a valid URL (http:// or https://) or a raw gRPC target (e.g. localhost:60051), with no query parameters and no trailing slash.\e[0m"
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
opts[:broker_url] = broker_url
|
|
73
|
+
|
|
74
|
+
if defined?(JRUBY_VERSION) || (defined?(RUBY_PLATFORM) && RUBY_PLATFORM =~ /java/)
|
|
75
|
+
raw_jruby_ver = defined?(JRUBY_VERSION) ? JRUBY_VERSION.to_s : '0'
|
|
76
|
+
jruby_ver = Gem::Version.new(raw_jruby_ver)
|
|
77
|
+
if jruby_ver < Gem::Version.new('9.3.0.0')
|
|
78
|
+
warn "[HyperProbe] Unsupported JRuby version (#{raw_jruby_ver}); JRuby 9.3+ is required."
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
unless jruby_debug_enabled?
|
|
83
|
+
warn "\e[1m\e[31m❌ [HyperProbe] CRITICAL: Failed to start agent on JRuby. The '--debug' launch flag (or JRUBY_OPTS=\"--debug\") is required for JRuby to preserve line boundaries and local variables. Please add 'ENV JRUBY_OPTS=\"--debug\"' to your deployment environment or start with 'jruby --debug'.\e[0m"
|
|
84
|
+
return nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless jruby_scope_preservation_enabled?
|
|
88
|
+
warn '[HyperProbe] Agent disabled: compiled JRuby probes require -J-Djruby.ir.passes=AddCallProtocolInstructions,AddMissingInitsPass and -J-Djruby.ir.jit.passes=AddCallProtocolInstructions,AddMissingInitsPass at process startup. These preserve locals with JIT enabled; benchmark their application-wide cost before deployment.'
|
|
89
|
+
return nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
require 'java'
|
|
94
|
+
java_ver_str = java.lang.System.getProperty('java.specification.version') || '11'
|
|
95
|
+
java_major = java_ver_str.sub(/^1\./, '').to_i
|
|
96
|
+
if java_major.positive? && java_major < 11
|
|
97
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] Unsupported Java runtime (#{java_ver_str}). HyperProbe on JRuby requires Java 11+ for native HTTP/2 gRPC.\e[0m"
|
|
98
|
+
return nil
|
|
99
|
+
end
|
|
100
|
+
rescue LoadError, StandardError
|
|
101
|
+
# Pass through if on non-JVM or property read is restricted
|
|
102
|
+
end
|
|
103
|
+
else
|
|
104
|
+
ruby_ver = Gem::Version.new(RUBY_VERSION)
|
|
105
|
+
if ruby_ver < Gem::Version.new('3.0.0')
|
|
106
|
+
warn "\e[1m\e[33m⚠️ [HyperProbe] Unsupported Ruby version (#{RUBY_VERSION}). HyperProbe requires Ruby 3.0+ for targeted line hooks.\e[0m"
|
|
107
|
+
return nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if opts.delete(:defer_start) || opts.delete(:deferStart)
|
|
112
|
+
@deferred_options = opts
|
|
113
|
+
return nil
|
|
114
|
+
end
|
|
115
|
+
@native_owner_pid = Process.pid
|
|
116
|
+
candidate = Agent.new(opts)
|
|
117
|
+
return nil if candidate.is_shutdown
|
|
118
|
+
if @stop_requested
|
|
119
|
+
candidate.shutdown
|
|
120
|
+
return nil
|
|
121
|
+
end
|
|
122
|
+
@instance = candidate
|
|
123
|
+
puts "[HyperProbe] Ruby agent successfully started (Version: #{VERSION}, ID: #{@instance.agent_id}, PID: #{@instance.owner_pid})."
|
|
124
|
+
@instance
|
|
125
|
+
ensure
|
|
126
|
+
@mutex.unlock if locked
|
|
127
|
+
end
|
|
128
|
+
rescue SignalException, SystemExit
|
|
129
|
+
candidate&.shutdown
|
|
130
|
+
raise
|
|
131
|
+
rescue Exception
|
|
132
|
+
nil
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def shutdown
|
|
136
|
+
reset_after_fork
|
|
137
|
+
@stop_requested = true
|
|
138
|
+
unless @mutex.try_lock
|
|
139
|
+
defer_shutdown
|
|
140
|
+
return nil
|
|
141
|
+
end
|
|
142
|
+
begin
|
|
143
|
+
inst = @instance
|
|
144
|
+
@instance = nil
|
|
145
|
+
ensure
|
|
146
|
+
@mutex.unlock
|
|
147
|
+
end
|
|
148
|
+
return unless inst
|
|
149
|
+
inst.shutdown
|
|
150
|
+
puts '[HyperProbe] Ruby agent successfully shut down.'
|
|
151
|
+
rescue ThreadError
|
|
152
|
+
defer_shutdown
|
|
153
|
+
nil
|
|
154
|
+
rescue SignalException, SystemExit
|
|
155
|
+
raise
|
|
156
|
+
rescue Exception
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def instance
|
|
161
|
+
@instance
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def after_fork
|
|
165
|
+
return unless Process.respond_to?(:fork) && !defined?(JRUBY_VERSION)
|
|
166
|
+
|
|
167
|
+
return @instance if @owner_pid == Process.pid
|
|
168
|
+
reset_after_fork
|
|
169
|
+
if @fork_disabled
|
|
170
|
+
warn '[HyperProbe] Inherited agent disabled. Use defer_start: true before preloading and after_fork in each worker.'
|
|
171
|
+
return nil
|
|
172
|
+
end
|
|
173
|
+
options = @deferred_options
|
|
174
|
+
@deferred_options = nil
|
|
175
|
+
start(options) if options
|
|
176
|
+
rescue SignalException, SystemExit
|
|
177
|
+
raise
|
|
178
|
+
rescue Exception
|
|
179
|
+
nil
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def wrap_lambda(options = {}, &handler)
|
|
183
|
+
return handler if defined?(LOAD_FAILED)
|
|
184
|
+
Lambda.wrap(options, &handler)
|
|
185
|
+
rescue SignalException, SystemExit
|
|
186
|
+
raise
|
|
187
|
+
rescue Exception
|
|
188
|
+
handler
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def stop_lambda
|
|
192
|
+
Lambda.stop if defined?(Lambda)
|
|
193
|
+
rescue SignalException, SystemExit
|
|
194
|
+
raise
|
|
195
|
+
rescue Exception
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def jruby_debug_enabled?
|
|
200
|
+
begin
|
|
201
|
+
require 'java'
|
|
202
|
+
return Java::OrgJruby::RubyInstanceConfig.FULL_TRACE_ENABLED == true
|
|
203
|
+
rescue StandardError, ScriptError, NameError
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
false
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def jruby_scope_preservation_enabled?
|
|
210
|
+
require 'java'
|
|
211
|
+
required = 'AddCallProtocolInstructions,AddMissingInitsPass'
|
|
212
|
+
Java::OrgJrubyUtilCli::Options::IR_COMPILER_PASSES.load.to_s == required &&
|
|
213
|
+
Java::OrgJrubyUtilCli::Options::IR_JIT_PASSES.load.to_s == required
|
|
214
|
+
rescue StandardError, ScriptError
|
|
215
|
+
false
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def valid_broker_url?(url)
|
|
219
|
+
return false if url.nil? || url.to_s.strip.empty?
|
|
220
|
+
|
|
221
|
+
raw = url.to_s.strip
|
|
222
|
+
if raw.start_with?('http://', 'https://')
|
|
223
|
+
begin
|
|
224
|
+
uri = URI.parse(raw)
|
|
225
|
+
return !raw.end_with?('/') && (uri.query.nil? || uri.query.empty?) && !uri.host.nil? && !uri.host.empty?
|
|
226
|
+
rescue URI::InvalidURIError
|
|
227
|
+
return false
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Raw gRPC target
|
|
232
|
+
!raw.include?('://') && !raw.include?('/') && !raw.include?('?')
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
private
|
|
236
|
+
|
|
237
|
+
def defer_shutdown
|
|
238
|
+
Thread.new do
|
|
239
|
+
inst = @mutex.synchronize do
|
|
240
|
+
current = @instance
|
|
241
|
+
@instance = nil
|
|
242
|
+
current
|
|
243
|
+
end
|
|
244
|
+
inst&.shutdown
|
|
245
|
+
rescue Exception
|
|
246
|
+
# Cleanup waits only on an agent worker, never on an application thread.
|
|
247
|
+
nil
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def reset_after_fork
|
|
252
|
+
return if @owner_pid == Process.pid
|
|
253
|
+
@owner_pid = Process.pid
|
|
254
|
+
@mutex = Mutex.new
|
|
255
|
+
if @native_owner_pid
|
|
256
|
+
@fork_disabled = true
|
|
257
|
+
@instance&.after_fork
|
|
258
|
+
@instance = nil
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def puts(message)
|
|
263
|
+
notice($stdout, message)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def warn(message)
|
|
267
|
+
notice($stderr, message)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def notice(stream, message)
|
|
271
|
+
if IO === stream
|
|
272
|
+
IO.instance_method(:write_nonblock).bind(stream).call("#{message}\n", exception: false)
|
|
273
|
+
elsif defined?(StringIO) && StringIO === stream
|
|
274
|
+
StringIO.instance_method(:write).bind(stream).call("#{message}\n")
|
|
275
|
+
end
|
|
276
|
+
rescue SignalException, SystemExit
|
|
277
|
+
raise
|
|
278
|
+
rescue Exception
|
|
279
|
+
nil
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
data/transport/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# JRuby gRPC Transport
|
|
2
|
+
|
|
3
|
+
Runtime baseline: MRI 3.0+ (tested 4.0.6), or JRuby 9.3+ on Java 11+ with the required
|
|
4
|
+
launch profile below. The Java artifact requires Ruby >= 2.6 and has no
|
|
5
|
+
`google-protobuf`, native `grpc`, or `ffi` runtime dependencies. MRI remains
|
|
6
|
+
Ruby >= 3.0 with `google-protobuf` >= 3.25, < 5 and `grpc` >= 1.50, < 2.
|
|
7
|
+
Choose a JDK supported by the selected JRuby.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
jruby --debug \
|
|
11
|
+
-J-Djruby.ir.passes=AddCallProtocolInstructions,AddMissingInitsPass \
|
|
12
|
+
-J-Djruby.ir.jit.passes=AddCallProtocolInstructions,AddMissingInitsPass app.rb
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`--debug` alone does not preserve all warmed line events/bindings. SDK startup
|
|
16
|
+
checks the effective Java field
|
|
17
|
+
`Java::OrgJruby::RubyInstanceConfig.FULL_TRACE_ENABLED` (dot access in Ruby code), and
|
|
18
|
+
`Java::OrgJrubyUtilCli::Options::IR_COMPILER_PASSES.load` plus
|
|
19
|
+
`Java::OrgJrubyUtilCli::Options::IR_JIT_PASSES.load` against the exact lists above.
|
|
20
|
+
It rejects unsafe profiles rather than changing process configuration. JIT stays
|
|
21
|
+
enabled without method exclusions.
|
|
22
|
+
|
|
23
|
+
`rake build` builds both `pkg/hyperprobe-agent-VERSION.gem` and
|
|
24
|
+
`pkg/hyperprobe-agent-VERSION-java.gem`. It needs Maven and JDK 11+ at build time;
|
|
25
|
+
Maven fetches the pinned dependencies then. `rake build:ruby` needs neither Java
|
|
26
|
+
nor Maven. `rake transport:build` prepares a source checkout for JRuby tests.
|
|
27
|
+
Generated JARs are ignored by Git and must be built before packaging Java.
|
|
28
|
+
The release script builds/validates both artifacts and tests both interpreters;
|
|
29
|
+
set `HYPERPROBE_JRUBY` if the JRuby executable is not named `jruby`.
|
|
30
|
+
|
|
31
|
+
Release both artifacts at the same version. Do not build a generic Ruby gem on
|
|
32
|
+
JRuby and assume its dependencies will change on installation. The gemspec's
|
|
33
|
+
explicit `HYPERPROBE_GEM_PLATFORM=ruby|java` build target controls its immutable
|
|
34
|
+
platform and dependency metadata; the default is always `ruby`. The source
|
|
35
|
+
Gemfile selects the interpreter's development target. Direct Java `gem build`
|
|
36
|
+
requires the previously built JAR. No Maven, jar-dependencies, native compiler,
|
|
37
|
+
or network download is used when the installed Java gem loads or connects.
|
|
38
|
+
|
|
39
|
+
The JAR contains grpc-java 1.84.0 (`grpc-netty-shaded` and `grpc-stub`), its
|
|
40
|
+
transitive dependencies, and patched `com.google.protobuf:protobuf-java:3.25.9`
|
|
41
|
+
core only. It does not bundle `protobuf-java-util` or Google's JRuby bridge.
|
|
42
|
+
Maven Shade relocates gRPC, Netty, Guava and supporting classes under
|
|
43
|
+
`co.hyperprobe.internal`; in particular, `com.google.protobuf` becomes
|
|
44
|
+
`co.hyperprobe.internal.google.protobuf`. Service descriptors and Apache notices
|
|
45
|
+
are merged. The supplemental [third-party notice](THIRD_PARTY_NOTICES.md)
|
|
46
|
+
preserves protobuf's upstream BSD 3-Clause license and source credits.
|
|
47
|
+
|
|
48
|
+
The transport bridge accepts protobuf bytes. For JRuby, an SDK-private Ruby
|
|
49
|
+
message adapter delegates encoding/decoding to Java `DynamicMessage` using the
|
|
50
|
+
generated descriptor of the existing SDK Ruby protocol schema. No generated
|
|
51
|
+
Java message models or Google Ruby runtime API are introduced. It neither creates
|
|
52
|
+
nor changes the host's `Google::Protobuf` module; a host application's separately
|
|
53
|
+
loaded Google protobuf remains independent. MRI retains its existing generated
|
|
54
|
+
Ruby protobuf path. The POM and Java bridge/codec sources ship in the Java gem
|
|
55
|
+
so its bundled dependency versions can be audited and rebuilt.
|
|
56
|
+
|
|
57
|
+
NIO sockets and JDK TLS/ALPN avoid platform-specific native binaries. HTTPS uses
|
|
58
|
+
the JVM's normal trust store and hostname verification, never an insecure trust
|
|
59
|
+
manager. Configure a private CA with the standard JVM trust-store options.
|
|
60
|
+
Raw `host:port` and `http://host:port` use plaintext gRPC; HTTPS uses TLS. IPv6
|
|
61
|
+
addresses in URLs must be bracketed. No HTTP/1.1 fallback is attempted.
|
|
62
|
+
|
|
63
|
+
grpc-java handles fragmentation, flow control, compression and terminal status.
|
|
64
|
+
Unary responses are accepted only after an OK terminal status. Inbound messages
|
|
65
|
+
are limited to 4 MiB, matching gRPC's usual default. Calls carry a finite deadline
|
|
66
|
+
including DNS, connect, TLS, request writes and response reads. The Ruby broker
|
|
67
|
+
starts the budget before constructing the request; the Java bridge also bounds
|
|
68
|
+
the caller's wait and cancels timed-out/interrupted calls. Shutdown cancels active
|
|
69
|
+
RPCs and asynchronously releases its daemon event-loop thread without waiting
|
|
70
|
+
for network peers. Errors propagate to the agent's fail-isolation boundary as
|
|
71
|
+
Ruby StandardError (RPC status with numeric `code`) or LoadError (missing or
|
|
72
|
+
incompatible bundled transport), never as an empty successful response.
|
|
73
|
+
|
|
74
|
+
Interop specs launch a local MRI C-core gRPC server. Run:
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
rake transport:build
|
|
78
|
+
ruby -S rspec spec/broker_transport_spec.rb spec/packaging_spec.rb
|
|
79
|
+
HYPERPROBE_MRI_RUBY=/path/to/mri/ruby jruby --debug \
|
|
80
|
+
-J-Djruby.ir.passes=AddCallProtocolInstructions,AddMissingInitsPass \
|
|
81
|
+
-J-Djruby.ir.jit.passes=AddCallProtocolInstructions,AddMissingInitsPass \
|
|
82
|
+
-S rspec spec/broker_transport_spec.rb spec/packaging_spec.rb
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The MRI executable must have `grpc`, `google-protobuf` and `rspec` installed.
|
|
86
|
+
The JRuby executable needs `rspec`, not the `google-protobuf` gem. Tests generate disposable
|
|
87
|
+
local TLS certificates and a trust store; production trust configuration is not
|
|
88
|
+
modified.
|
|
89
|
+
|
|
90
|
+
Clean packaged Bundler installs have succeeded on JRuby 9.3 and 10.1 without
|
|
91
|
+
forced installs or Google Ruby protobuf load-path overrides. JRuby 9.3 needs
|
|
92
|
+
Ruby 2.6-compatible Bundler 2.4. Do not reuse a frozen Ruby 4 development
|
|
93
|
+
lockfile on older runtimes without resolving for that runtime. The release
|
|
94
|
+
script now supplies the complete required profile to its JRuby test invocation.
|
|
95
|
+
|
|
96
|
+
`HYPERPROBE_TEST_LIB=/installed/gem/lib` runs the interop clients against an
|
|
97
|
+
installed package rather than the source checkout. The test server remains the
|
|
98
|
+
independent MRI C-core fixture.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Third-Party Notices
|
|
2
|
+
|
|
3
|
+
The Java artifact bundles third-party libraries in
|
|
4
|
+
`lib/hyperprobe/jars/hyperprobe-grpc.jar`. Its build inputs and pinned versions
|
|
5
|
+
are recorded in `transport/pom.xml`; upstream license/notice resources are also
|
|
6
|
+
carried in the shaded JAR. This supplemental notice preserves the protobuf
|
|
7
|
+
copyright, conditions, and disclaimer for binary redistribution. It does not
|
|
8
|
+
replace other bundled libraries' notices or change the SDK's own license.
|
|
9
|
+
|
|
10
|
+
## Protocol Buffers Java Core 3.25.9
|
|
11
|
+
|
|
12
|
+
- Component: `com.google.protobuf:protobuf-java:3.25.9` (core only, not
|
|
13
|
+
`protobuf-java-util` or Google's JRuby bridge).
|
|
14
|
+
- Upstream project and source: <https://github.com/protocolbuffers/protobuf/tree/v25.9>.
|
|
15
|
+
- Official license: <https://github.com/protocolbuffers/protobuf/blob/v25.9/LICENSE>.
|
|
16
|
+
- Retrieved from <https://raw.githubusercontent.com/protocolbuffers/protobuf/v25.9/LICENSE>
|
|
17
|
+
on 2026-09-07; license text reproduced below verbatim.
|
|
18
|
+
- License: BSD 3-Clause. This permits source and binary redistribution, including
|
|
19
|
+
modified versions, subject to the notice, disclaimer, and non-endorsement
|
|
20
|
+
conditions below. The SDK shades `com.google.protobuf` to
|
|
21
|
+
`co.hyperprobe.internal.google.protobuf`; upstream credit and license terms
|
|
22
|
+
remain applicable.
|
|
23
|
+
|
|
24
|
+
```text
|
|
25
|
+
Copyright 2008 Google Inc. All rights reserved.
|
|
26
|
+
|
|
27
|
+
Redistribution and use in source and binary forms, with or without
|
|
28
|
+
modification, are permitted provided that the following conditions are
|
|
29
|
+
met:
|
|
30
|
+
|
|
31
|
+
* Redistributions of source code must retain the above copyright
|
|
32
|
+
notice, this list of conditions and the following disclaimer.
|
|
33
|
+
* Redistributions in binary form must reproduce the above
|
|
34
|
+
copyright notice, this list of conditions and the following disclaimer
|
|
35
|
+
in the documentation and/or other materials provided with the
|
|
36
|
+
distribution.
|
|
37
|
+
* Neither the name of Google Inc. nor the names of its
|
|
38
|
+
contributors may be used to endorse or promote products derived from
|
|
39
|
+
this software without specific prior written permission.
|
|
40
|
+
|
|
41
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
42
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
43
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
44
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
45
|
+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
46
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
47
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
48
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
49
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
50
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
51
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
52
|
+
|
|
53
|
+
Code generated by the Protocol Buffer compiler is owned by the owner
|
|
54
|
+
of the input file used when generating it. This code is not
|
|
55
|
+
standalone and requires a support library to be linked with it. This
|
|
56
|
+
support library is itself covered by the above license.
|
|
57
|
+
```
|
data/transport/pom.xml
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
3
|
+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
4
|
+
<modelVersion>4.0.0</modelVersion>
|
|
5
|
+
<groupId>co.hyperprobe</groupId>
|
|
6
|
+
<artifactId>hyperprobe-grpc</artifactId>
|
|
7
|
+
<version>1.0.0</version>
|
|
8
|
+
<properties>
|
|
9
|
+
<maven.compiler.release>11</maven.compiler.release>
|
|
10
|
+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
11
|
+
<project.build.outputTimestamp>2026-09-07T00:00:00Z</project.build.outputTimestamp>
|
|
12
|
+
<grpc.version>1.84.0</grpc.version>
|
|
13
|
+
<protobuf.version>3.25.9</protobuf.version>
|
|
14
|
+
</properties>
|
|
15
|
+
<dependencies>
|
|
16
|
+
<dependency>
|
|
17
|
+
<groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>${protobuf.version}</version>
|
|
18
|
+
</dependency>
|
|
19
|
+
<dependency>
|
|
20
|
+
<groupId>io.grpc</groupId><artifactId>grpc-netty-shaded</artifactId><version>${grpc.version}</version>
|
|
21
|
+
</dependency>
|
|
22
|
+
<dependency>
|
|
23
|
+
<groupId>io.grpc</groupId><artifactId>grpc-stub</artifactId><version>${grpc.version}</version>
|
|
24
|
+
</dependency>
|
|
25
|
+
</dependencies>
|
|
26
|
+
<build>
|
|
27
|
+
<finalName>hyperprobe-grpc</finalName>
|
|
28
|
+
<plugins>
|
|
29
|
+
<plugin>
|
|
30
|
+
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.1</version>
|
|
31
|
+
</plugin>
|
|
32
|
+
<plugin>
|
|
33
|
+
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.4.2</version>
|
|
34
|
+
</plugin>
|
|
35
|
+
<plugin>
|
|
36
|
+
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.6.1</version>
|
|
37
|
+
<executions>
|
|
38
|
+
<execution>
|
|
39
|
+
<phase>package</phase><goals><goal>shade</goal></goals>
|
|
40
|
+
<configuration>
|
|
41
|
+
<createDependencyReducedPom>false</createDependencyReducedPom>
|
|
42
|
+
<relocations>
|
|
43
|
+
<relocation><pattern>io.grpc</pattern><shadedPattern>co.hyperprobe.internal.grpc</shadedPattern></relocation>
|
|
44
|
+
<relocation><pattern>com.google</pattern><shadedPattern>co.hyperprobe.internal.google</shadedPattern></relocation>
|
|
45
|
+
<relocation><pattern>io.perfmark</pattern><shadedPattern>co.hyperprobe.internal.perfmark</shadedPattern></relocation>
|
|
46
|
+
<relocation><pattern>org.codehaus</pattern><shadedPattern>co.hyperprobe.internal.codehaus</shadedPattern></relocation>
|
|
47
|
+
<relocation><pattern>javax.annotation</pattern><shadedPattern>co.hyperprobe.internal.annotation</shadedPattern></relocation>
|
|
48
|
+
<relocation><pattern>org.jspecify</pattern><shadedPattern>co.hyperprobe.internal.jspecify</shadedPattern></relocation>
|
|
49
|
+
</relocations>
|
|
50
|
+
<filters>
|
|
51
|
+
<filter>
|
|
52
|
+
<artifact>*:*</artifact>
|
|
53
|
+
<excludes>
|
|
54
|
+
<exclude>META-INF/*.SF</exclude><exclude>META-INF/*.RSA</exclude><exclude>META-INF/*.DSA</exclude>
|
|
55
|
+
<exclude>META-INF/native/**</exclude><exclude>module-info.class</exclude>
|
|
56
|
+
<exclude>META-INF/versions/**/module-info.class</exclude>
|
|
57
|
+
</excludes>
|
|
58
|
+
</filter>
|
|
59
|
+
</filters>
|
|
60
|
+
<transformers>
|
|
61
|
+
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
|
|
62
|
+
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer"/>
|
|
63
|
+
<transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer"/>
|
|
64
|
+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
65
|
+
<manifestEntries>
|
|
66
|
+
<Multi-Release>true</Multi-Release>
|
|
67
|
+
<HyperProbe-Protobuf-Version>${protobuf.version}</HyperProbe-Protobuf-Version>
|
|
68
|
+
<HyperProbe-Grpc-Version>${grpc.version}</HyperProbe-Grpc-Version>
|
|
69
|
+
</manifestEntries>
|
|
70
|
+
</transformer>
|
|
71
|
+
</transformers>
|
|
72
|
+
</configuration>
|
|
73
|
+
</execution>
|
|
74
|
+
</executions>
|
|
75
|
+
</plugin>
|
|
76
|
+
</plugins>
|
|
77
|
+
</build>
|
|
78
|
+
</project>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
package co.hyperprobe.transport;
|
|
2
|
+
|
|
3
|
+
import com.google.common.util.concurrent.ListenableFuture;
|
|
4
|
+
import io.grpc.CallOptions;
|
|
5
|
+
import io.grpc.Channel;
|
|
6
|
+
import io.grpc.ClientInterceptors;
|
|
7
|
+
import io.grpc.Deadline;
|
|
8
|
+
import io.grpc.ManagedChannel;
|
|
9
|
+
import io.grpc.Metadata;
|
|
10
|
+
import io.grpc.MethodDescriptor;
|
|
11
|
+
import io.grpc.Status;
|
|
12
|
+
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
|
13
|
+
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
|
14
|
+
import io.grpc.netty.shaded.io.netty.channel.nio.NioEventLoopGroup;
|
|
15
|
+
import io.grpc.netty.shaded.io.netty.channel.socket.nio.NioSocketChannel;
|
|
16
|
+
import io.grpc.netty.shaded.io.netty.handler.ssl.SslProvider;
|
|
17
|
+
import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder;
|
|
18
|
+
import io.grpc.netty.shaded.io.netty.util.concurrent.DefaultThreadFactory;
|
|
19
|
+
import io.grpc.stub.ClientCalls;
|
|
20
|
+
import io.grpc.stub.MetadataUtils;
|
|
21
|
+
import java.io.ByteArrayInputStream;
|
|
22
|
+
import java.io.IOException;
|
|
23
|
+
import java.io.InputStream;
|
|
24
|
+
import java.util.concurrent.ExecutionException;
|
|
25
|
+
import java.util.concurrent.TimeUnit;
|
|
26
|
+
import java.util.concurrent.TimeoutException;
|
|
27
|
+
|
|
28
|
+
/** Byte-only boundary: protobuf encoding remains in Ruby, HTTP/2 belongs to grpc-java. */
|
|
29
|
+
public final class GrpcTransport {
|
|
30
|
+
private static final MethodDescriptor.Marshaller<byte[]> BYTES = new MethodDescriptor.Marshaller<byte[]>() {
|
|
31
|
+
public InputStream stream(byte[] value) {
|
|
32
|
+
return new ByteArrayInputStream(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public byte[] parse(InputStream stream) {
|
|
36
|
+
try {
|
|
37
|
+
return stream.readAllBytes();
|
|
38
|
+
} catch (IOException e) {
|
|
39
|
+
throw Status.INTERNAL.withCause(e).asRuntimeException();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
private final NioEventLoopGroup eventLoop;
|
|
45
|
+
private final ManagedChannel channel;
|
|
46
|
+
private final Channel authenticatedChannel;
|
|
47
|
+
|
|
48
|
+
public GrpcTransport(String host, int port, boolean secure, String serviceId, boolean keepAlive)
|
|
49
|
+
throws javax.net.ssl.SSLException {
|
|
50
|
+
eventLoop = new NioEventLoopGroup(1, new DefaultThreadFactory("hyperprobe-grpc", true));
|
|
51
|
+
try {
|
|
52
|
+
// Explicit NIO/JDK TLS avoids platform-specific native libraries in the gem.
|
|
53
|
+
NettyChannelBuilder builder = NettyChannelBuilder.forAddress(host, port)
|
|
54
|
+
.eventLoopGroup(eventLoop).channelType(NioSocketChannel.class)
|
|
55
|
+
.maxInboundMessageSize(4 * 1024 * 1024).disableRetry();
|
|
56
|
+
if (secure) {
|
|
57
|
+
builder.sslContext(GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.JDK).build());
|
|
58
|
+
} else {
|
|
59
|
+
builder.usePlaintext();
|
|
60
|
+
}
|
|
61
|
+
if (keepAlive) {
|
|
62
|
+
builder.keepAliveTime(60, TimeUnit.SECONDS).keepAliveTimeout(20, TimeUnit.SECONDS)
|
|
63
|
+
.keepAliveWithoutCalls(true);
|
|
64
|
+
}
|
|
65
|
+
Metadata metadata = new Metadata();
|
|
66
|
+
metadata.put(Metadata.Key.of("x-hp-service-id", Metadata.ASCII_STRING_MARSHALLER), serviceId);
|
|
67
|
+
channel = builder.build();
|
|
68
|
+
authenticatedChannel = ClientInterceptors.intercept(channel,
|
|
69
|
+
MetadataUtils.newAttachHeadersInterceptor(metadata));
|
|
70
|
+
} catch (RuntimeException | Error | javax.net.ssl.SSLException e) {
|
|
71
|
+
eventLoop.shutdownGracefully(0, 1, TimeUnit.SECONDS);
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public byte[] call(String method, byte[] request, long timeoutNanos) throws RpcException {
|
|
77
|
+
Deadline deadline = Deadline.after(timeoutNanos, TimeUnit.NANOSECONDS);
|
|
78
|
+
MethodDescriptor<byte[], byte[]> descriptor = MethodDescriptor.<byte[], byte[]>newBuilder()
|
|
79
|
+
.setType(MethodDescriptor.MethodType.UNARY).setFullMethodName(method)
|
|
80
|
+
.setRequestMarshaller(BYTES).setResponseMarshaller(BYTES).build();
|
|
81
|
+
ListenableFuture<byte[]> future = ClientCalls.futureUnaryCall(
|
|
82
|
+
authenticatedChannel.newCall(descriptor, CallOptions.DEFAULT.withDeadline(deadline)), request);
|
|
83
|
+
try {
|
|
84
|
+
// The caller is bounded even during DNS, connect, TLS, or a stalled peer.
|
|
85
|
+
// A unary future completes only after terminal grpc-status, not the first DATA frame.
|
|
86
|
+
return future.get(Math.max(0, deadline.timeRemaining(TimeUnit.NANOSECONDS)), TimeUnit.NANOSECONDS);
|
|
87
|
+
} catch (TimeoutException e) {
|
|
88
|
+
throw new RpcException(Status.DEADLINE_EXCEEDED);
|
|
89
|
+
} catch (InterruptedException e) {
|
|
90
|
+
Thread.currentThread().interrupt();
|
|
91
|
+
throw new RpcException(Status.CANCELLED);
|
|
92
|
+
} catch (ExecutionException e) {
|
|
93
|
+
throw new RpcException(Status.fromThrowable(e.getCause()));
|
|
94
|
+
} finally {
|
|
95
|
+
if (!future.isDone()) {
|
|
96
|
+
future.cancel(true);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public void shutdown() {
|
|
102
|
+
channel.shutdownNow();
|
|
103
|
+
eventLoop.shutdownGracefully(0, 1, TimeUnit.SECONDS);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public static final class RpcException extends Exception {
|
|
107
|
+
private final int code;
|
|
108
|
+
|
|
109
|
+
RpcException(Status status) {
|
|
110
|
+
super(status.getCode() + ": " + status.getDescription());
|
|
111
|
+
code = status.getCode().value();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public int getCode() {
|
|
115
|
+
return code;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package co.hyperprobe.transport;
|
|
2
|
+
|
|
3
|
+
import com.google.protobuf.ByteString;
|
|
4
|
+
import com.google.protobuf.CodedInputStream;
|
|
5
|
+
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
|
|
6
|
+
import com.google.protobuf.Descriptors;
|
|
7
|
+
import com.google.protobuf.DynamicMessage;
|
|
8
|
+
import java.io.IOException;
|
|
9
|
+
|
|
10
|
+
/** SDK-private protobuf runtime. Maven relocates every protobuf type in this API. */
|
|
11
|
+
public final class ProtoCodec {
|
|
12
|
+
public static final int MAX_BYTES = 4 * 1024 * 1024;
|
|
13
|
+
private final Descriptors.FileDescriptor file;
|
|
14
|
+
|
|
15
|
+
public ProtoCodec(byte[] descriptor) throws IOException, Descriptors.DescriptorValidationException {
|
|
16
|
+
file = Descriptors.FileDescriptor.buildFrom(
|
|
17
|
+
FileDescriptorProto.parseFrom(descriptor), new Descriptors.FileDescriptor[0]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public Descriptors.FileDescriptor getFile() {
|
|
21
|
+
return file;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public DynamicMessage.Builder builder(Descriptors.Descriptor descriptor) {
|
|
25
|
+
return DynamicMessage.newBuilder(descriptor);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public ByteString bytes(byte[] value) {
|
|
29
|
+
return ByteString.copyFrom(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public DynamicMessage decode(Descriptors.Descriptor descriptor, byte[] bytes) throws IOException {
|
|
33
|
+
if (bytes.length > MAX_BYTES) {
|
|
34
|
+
throw new IllegalArgumentException("Protobuf payload exceeds 4 MiB");
|
|
35
|
+
}
|
|
36
|
+
CodedInputStream input = CodedInputStream.newInstance(bytes);
|
|
37
|
+
input.setSizeLimit(MAX_BYTES);
|
|
38
|
+
input.setRecursionLimit(64);
|
|
39
|
+
DynamicMessage message = DynamicMessage.parseFrom(descriptor, input);
|
|
40
|
+
input.checkLastTagWas(0);
|
|
41
|
+
return message;
|
|
42
|
+
}
|
|
43
|
+
}
|