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.
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
@@ -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
+ }