morio_bridge 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f0fce2880ec932e61fa2a9d3d80633b0c9da60ec447e045648ffd1dab279515e
4
+ data.tar.gz: 5fd1cb1b6e08879d422ea65f7628ecd3853da98e97697055144bf0feaedab86d
5
+ SHA512:
6
+ metadata.gz: 706bb1b76e072fb7cb5f4ecdf20e7e06bfc1f2ee074df488047bebe8970c4bdf05158598a4659b1af284039f1bf6911e6a8d052469749a29023a7fdc7dd80ff7
7
+ data.tar.gz: 51c0fc4554e5a12871e36f400c1a15b317c4a7d12fe3cf9c193c1ec19f3483a6180775c3d3f49c0d110217d954828d502bf397d4b7b645692dca52a69b854dec
@@ -0,0 +1,58 @@
1
+ #!/bin/bash
2
+
3
+
4
+ #===============================================
5
+ #
6
+ # Install dependencies
7
+ #
8
+ #===============================================
9
+ # Install Bun
10
+ curl -fsSL https://bun.sh/install | bash -s "bun-v1.2.13"
11
+
12
+
13
+ # install global dependencies
14
+ bun install -g prisma@6.8.2 @prisma/client@6.8.2 @prisma/adapter-pg@6.8.2 fastest-validator@1.19.1 pg@8.16.0
15
+
16
+ # add bun to the path and save it to the .bashrc and source it
17
+ export PATH="$HOME/.bun/bin:$PATH"
18
+ echo "export PATH=\"$HOME/.bun/bin:$PATH\"" >> ~/.bashrc
19
+ source ~/.bashrc
20
+
21
+ # upgrade bun to the latest version
22
+ bun upgrade
23
+
24
+ # Verify installation
25
+ echo "dependencies installed successfully"
26
+ echo "installed bun v$(bun --version)"
27
+
28
+
29
+ #===============================================
30
+ #
31
+ # Build plugins
32
+ #
33
+ #===============================================
34
+ echo "building plugins ..."
35
+
36
+ mkdir -p $HOME/.morio/bin
37
+ rm -rf $HOME/.morio/bin/server-*
38
+ rm -rf bin/server-*
39
+
40
+ IS_DOCKER="$1"
41
+ PLATFORM="$2"
42
+ ARCH="$3"
43
+ OUTPUT=$HOME/.morio/bin/bridge_server.js
44
+
45
+
46
+ cd ./server
47
+
48
+ bun install
49
+ bun build --target=bun index.ts --outfile "$OUTPUT"
50
+
51
+
52
+
53
+
54
+
55
+
56
+
57
+
58
+
@@ -0,0 +1,482 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "pathname"
5
+ require "fileutils"
6
+ require "tmpdir"
7
+ require "socket"
8
+ require "json"
9
+ require "logger"
10
+
11
+ class Error < StandardError; end
12
+ class TimeoutError < Error; end
13
+
14
+ # Main module for the gem
15
+ module MorioBridge
16
+ VERSION = "0.1.0"
17
+
18
+ class Doctor
19
+ def self.run
20
+ stats = JSON.pretty_generate({
21
+ current_dir: Dir.pwd,
22
+ bun_path: `which bun`.strip,
23
+ bun_version: `bun --version`.strip
24
+ })
25
+
26
+ puts stats
27
+ end
28
+ end
29
+
30
+ class Client
31
+ attr_reader :instance
32
+
33
+ def initialize(base_socket_dir = nil)
34
+ @server_path = File.expand_path(File.join(Dir.home, ".morio/bin/bridge_server.js"))
35
+ @bun_path = File.expand_path(File.join(Dir.home, ".bun/bin/bun"))
36
+ @base_socket_dir = base_socket_dir || Dir.mktmpdir("prisma")
37
+ @socket_paths = fetch_socket_paths(6)
38
+ @socket_connections = []
39
+ @socket_status = []
40
+ @socket_index = 0
41
+ @pid = nil
42
+ @started = false
43
+ @starting = false
44
+ @instance = nil
45
+ @logger = Logger.new($stdout)
46
+
47
+ return unless !@testing_mode && defined?(Morio)
48
+
49
+ # morio global logger
50
+ @logger = Morio.logger
51
+ end
52
+
53
+ #
54
+ # [ Lifecycle ]
55
+ #
56
+ def start
57
+ @starting = true
58
+
59
+ begin
60
+ FileUtils.mkdir_p(@base_socket_dir)
61
+ FileUtils.chmod(0o700, @base_socket_dir)
62
+
63
+ env = {
64
+ "MORIO_RB_BUN_SOCKETS" => @socket_paths.join(","),
65
+ "MORIO_RB_PWD" => Dir.pwd
66
+ }
67
+
68
+ # Start server in background thread immediately
69
+ server_thread = Thread.new do
70
+ # Create pipes for stdout and stderr
71
+ out_read, out_write = IO.pipe
72
+ err_read, err_write = IO.pipe
73
+
74
+ @pid = Process.spawn(
75
+ env,
76
+ "#{@bun_path} #{@server_path}",
77
+ err: err_write,
78
+ out: out_write,
79
+ pgroup: true,
80
+ close_others: true
81
+ )
82
+
83
+ # Close write ends in parent since they are not needed as we dont write anything to the child process
84
+ out_write.close
85
+ err_write.close
86
+
87
+ log_server_output(out_read, err_read)
88
+
89
+ Process.detach(@pid)
90
+ end
91
+
92
+ # Start health check in parallel
93
+ health_thread = Thread.new { server_healthy? }
94
+
95
+ # Wait for both to complete
96
+ server_thread.join
97
+ health_thread.join
98
+
99
+ # Initialize sockets...
100
+ threads = @socket_paths.map do |socket_path|
101
+ Thread.new do
102
+ socket_healthy?(socket_path)
103
+ socket = UNIXSocket.new(socket_path)
104
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
105
+ [socket, true]
106
+ end
107
+ end
108
+ @socket_connections, @socket_status = threads.map(&:value).transpose
109
+
110
+ @started = true
111
+
112
+ # save instance
113
+ @instance = self
114
+
115
+ at_exit { stop }
116
+ ensure
117
+ @starting = false
118
+ end
119
+ end
120
+
121
+ def stop
122
+ @logger.debug "Stopping morio_bridge ..."
123
+
124
+ # close all the socket connections
125
+ @socket_connections.each do |connection|
126
+ connection.close
127
+ rescue StandardError
128
+ nil
129
+ end
130
+
131
+ # kill the unix socket cluster process
132
+ Process.kill("TERM", @pid) if @pid
133
+
134
+ # remove the socket directory
135
+ FileUtils.remove_entry(@base_socket_dir) if File.directory?(@base_socket_dir)
136
+
137
+ # set the started flag to false
138
+ @started = false
139
+
140
+ @logger.debug "Stopped the morio_bridge"
141
+ rescue StandardError
142
+ nil
143
+ end
144
+
145
+ #
146
+ # [ Message Handler ]
147
+ #
148
+ def request(data, skip_client_start: false)
149
+ # start the client if it's not already started
150
+ start unless @started || skip_client_start
151
+
152
+ # set the max number of retries
153
+ max_retries = 3
154
+ retries = 0
155
+ timeout = 0.05
156
+
157
+ loop do
158
+ # select a socket in round robin fashion
159
+ socket = select_socket
160
+
161
+ begin
162
+ # raise an error if the socket is closed
163
+ raise IOError, "Socket is closed" if socket.closed?
164
+
165
+ #=====================================
166
+ #
167
+ # Send Request
168
+ #
169
+ #=====================================
170
+ # generate the request body
171
+ request = to_socket_format(data)
172
+
173
+ # write the request to the socket
174
+ socket.write_nonblock(request)
175
+
176
+ #=====================================
177
+ #
178
+ # Read Response
179
+ #
180
+ #=====================================
181
+ # wait for the socket to be readable
182
+ readable = socket.wait_readable(timeout)
183
+
184
+ # raise an error if the socket is not readable
185
+ unless readable
186
+ timeout = 0.002
187
+ raise TimeoutError, "Command timed out"
188
+ end
189
+
190
+ # read the response from the socket into a string buffer
191
+ buffer = String.new(capacity: 4096)
192
+ done = false
193
+
194
+ while (chunk = socket.readpartial(4096))
195
+ buffer << chunk
196
+ done = true if chunk.include?("\r\n\r\n")
197
+ break if done
198
+ end
199
+
200
+ #=====================================
201
+ #
202
+ # Parse Response
203
+ #
204
+ #=====================================
205
+ body = to_hash(from_socket_format(buffer))
206
+ return body
207
+ rescue Errno::EPIPE, IOError, TimeoutError => _e
208
+ # retry the connection in case of a communication error
209
+ retry_failed_connection(socket)
210
+
211
+ # increment the retries
212
+ retries += 1
213
+
214
+ # raise an error if the retries exceed the max retries
215
+ raise Error, "No healthy sockets after #{max_retries} retries" if retries >= max_retries
216
+
217
+ # sleep for a short period before retrying - exponential backoff
218
+ sleep(0.05 * (2**retries))
219
+ retry
220
+ end
221
+ end
222
+ end
223
+
224
+ #
225
+ # [ Benchmark ]
226
+ #
227
+ def benchmark(send_rate_per_second: 600_000, requests_to_be_sent: 10_000)
228
+ basic_test = lambda do
229
+ request({ command: "create", body: { name: "John Doe" } })
230
+ end
231
+
232
+ speed_test = lambda do
233
+ error_count = 0
234
+ durations = []
235
+
236
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
237
+
238
+ loop_count = 0
239
+
240
+ loop do
241
+ break if loop_count > requests_to_be_sent
242
+
243
+ begin
244
+ duration, _result = UnixClient.with_timing(&basic_test)
245
+ durations << duration
246
+ rescue Error => _e
247
+ error_count += 1
248
+ end
249
+
250
+ loop_count += 1
251
+
252
+ sleep(1.0 / send_rate_per_second)
253
+ end
254
+
255
+ duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - start) / 1_000_000.0
256
+ rps = requests_to_be_sent / duration_ms * 1000
257
+
258
+ {
259
+ requests: requests_to_be_sent,
260
+ send_rate_per_second: send_rate_per_second,
261
+ duration_ms: duration_ms.round(2),
262
+ avg_latency_ms: (durations.sum / durations.size).round(2),
263
+ rps: rps.round(0),
264
+ errors: error_count
265
+ }
266
+ end
267
+
268
+ # Run tests in parallel
269
+ speed_thread = Thread.new { UnixClient.with_timing(&speed_test) }
270
+ total_duration, results = speed_thread.value
271
+
272
+ result = JSON.pretty_generate({ results:, total_duration: })
273
+
274
+ # make result gray and dim and log it
275
+ @logger.info result
276
+ end
277
+
278
+ #
279
+ # [ Benchmark helpers ]
280
+ #
281
+ def self.with_timing(&proc)
282
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
283
+ result = proc.call
284
+ end_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
285
+ duration_ms = ((end_ns - start) / 1_000_000.0)
286
+
287
+ [duration_ms, result]
288
+ end
289
+
290
+ private
291
+
292
+ #
293
+ # [ Child process Logging helpers ]
294
+ #
295
+ def log_server_output(out_read, err_read)
296
+ # Start threads to read from pipes
297
+ Thread.new { handle_server_stream(out_read, :info) }
298
+ Thread.new { handle_server_stream(err_read, :error) }
299
+ end
300
+
301
+ def handle_server_stream(stream, log_level)
302
+ buffer = String.new
303
+ while (line = stream.gets)
304
+ buffer << line
305
+ next unless line.end_with?("\n")
306
+
307
+ message = buffer.chomp
308
+ # First try as single JSON line
309
+ begin
310
+ parsed = JSON.parse(message)
311
+ @logger.send(log_level, JSON.pretty_generate(parsed))
312
+ buffer.clear
313
+ rescue JSON::ParserError
314
+ # If single line parse failed, check if we have complete JSON
315
+ begin
316
+ parsed = JSON.parse(buffer)
317
+ @logger.send(log_level, JSON.pretty_generate(parsed))
318
+ buffer.clear
319
+ rescue JSON::ParserError
320
+ # Not complete JSON yet, or plain text
321
+ # Only log and clear if it's definitely not JSON
322
+
323
+ # turn off Metrics/BlockNesting:
324
+ # rubocop:disable Metrics/BlockNesting
325
+ unless buffer.match?(/[\{\[]/u)
326
+ @logger.send(log_level, message)
327
+ buffer.clear
328
+ end
329
+ # rubocop:enable Metrics/BlockNesting
330
+ end
331
+ end
332
+ end
333
+ # Log any remaining buffer content
334
+ @logger.send(log_level, buffer) unless buffer.empty?
335
+ end
336
+
337
+ #
338
+ # [ Socket helpers ]
339
+ #
340
+ def fetch_socket_paths(socket_server_count)
341
+ (0..(socket_server_count - 1)).map do |i|
342
+ File.join(@base_socket_dir, "prisma#{i}.sock")
343
+ end
344
+ end
345
+
346
+ def select_socket
347
+ connection = @socket_connections[@socket_index]
348
+
349
+ @socket_index = (@socket_index + 1) % @socket_connections.length
350
+ return connection if @socket_status[@socket_connections.index(connection) || 0]
351
+
352
+ @socket_paths.length.times do
353
+ connection = @socket_connections[@socket_index]
354
+
355
+ @socket_index = (@socket_index + 1) % @socket_connections.length
356
+ return connection if @socket_status[@socket_connections.index(connection) || 0]
357
+ end
358
+ raise Error, "No healthy sockets available"
359
+ end
360
+
361
+ def retry_failed_connection(failed_socket)
362
+ # get the index of the failed socket and return if it doesn't exist
363
+ failed_connection_id = @socket_connections.index(failed_socket)
364
+ return unless failed_connection_id
365
+
366
+ # set the socket status to false marking it as unhealthy
367
+ @socket_status[failed_connection_id] = false
368
+
369
+ # close the failed socket
370
+ begin
371
+ @socket_connections[failed_connection_id].close
372
+ rescue StandardError
373
+ nil
374
+ end
375
+
376
+ # get the socket path for the failed socket
377
+ socket_path = @socket_paths[failed_connection_id]
378
+
379
+ # try to reconnect to the socket
380
+ begin
381
+ @socket_connections[failed_connection_id] = UNIXSocket.new(socket_path)
382
+
383
+ # set the socket status to true marking it as healthy if the connection is successful
384
+ @socket_status[failed_connection_id] = true
385
+ rescue Errno::ENOENT, Errno::ECONNREFUSED => e
386
+ @logger.error "Failed to reconnect to socket #{socket_path}: #{e.message}"
387
+ end
388
+ end
389
+
390
+ #
391
+ # [ Health check helpers ]
392
+ #
393
+ def socket_healthy?(socket_path)
394
+ max_connection_attempts = 50
395
+ connection_attempt = 0
396
+ base_delay_s = 0.05
397
+
398
+ while connection_attempt < max_connection_attempts
399
+ begin
400
+ # attempt to connect to the socket if the connection is successful, close the socket and return
401
+ socket = UNIXSocket.new(socket_path)
402
+ socket.close
403
+ return true
404
+ rescue Errno::ENOENT, Errno::ECONNREFUSED => _e
405
+ # exponential backoff
406
+ sleep(base_delay_s * (2**connection_attempt))
407
+ end
408
+
409
+ # increment the connection attempt
410
+ connection_attempt += 1
411
+ end
412
+
413
+ # raise an error if the connection attempt fails and exceeds the max connection attempts
414
+ raise TimeoutError, "Could not connect to socket #{socket_path} after #{max_connection_attempts} attempts"
415
+ end
416
+
417
+ def server_healthy?
418
+ max_connection_attempts = 50
419
+ connection_attempt = 0
420
+ base_delay_s = 0.05 # Reduced from 0.1
421
+
422
+ while connection_attempt < max_connection_attempts
423
+ begin
424
+ socket = UNIXSocket.new(@socket_paths.first)
425
+ socket.write_nonblock("GET /health HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
426
+
427
+ # Reduced timeout
428
+ if socket.wait_readable(0.05)
429
+ response = socket.read_nonblock(1024) # Smaller read buffer
430
+ if response.include?("200") # Simpler check
431
+ socket.close
432
+ return true
433
+ end
434
+ end
435
+ socket.close
436
+ rescue Errno::ENOENT, Errno::ECONNREFUSED, IOError => _e
437
+ sleep(base_delay_s * (1.5**connection_attempt)) # Less aggressive backoff
438
+ end
439
+ connection_attempt += 1
440
+ end
441
+
442
+ raise TimeoutError, "Server failed to start"
443
+ end
444
+
445
+ #
446
+ # [ socket format helpers ]
447
+ #
448
+ def from_socket_format(buffer)
449
+ lines = buffer.lines
450
+ status_line = lines[0]
451
+ matches = status_line.match(%r{HTTP/1\.1 (\d+)})
452
+ status_code = matches&.captures&.first.to_i || nil
453
+ body_start = lines.index("\r\n") || lines.length
454
+ body = lines[(body_start + 1)..]&.join || ""
455
+
456
+ # raise an error if the status code is not 200
457
+ raise Error, "Executable returned #{status_code}: #{body}" unless status_code == 200
458
+
459
+ body
460
+ end
461
+
462
+ def to_socket_format(data)
463
+ body = JSON.generate(data)
464
+
465
+ [
466
+ "POST /prisma/0/call HTTP/1.1",
467
+ "Content-Type: application/json",
468
+ "Content-Length: #{body.length}",
469
+ "",
470
+ body
471
+ ].join("\r\n")
472
+ end
473
+
474
+ def to_hash(body)
475
+ # Validate JSON response
476
+
477
+ JSON.parse(body)
478
+ rescue JSON::ParserError => e
479
+ raise Error, "Invalid JSON response: #{e.message}, body: #{body.inspect}"
480
+ end
481
+ end
482
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "morio_bridge"
5
+ require "English"
6
+
7
+ Gem.post_install do |installer|
8
+ if installer.spec.name == "morio_bridge"
9
+ post_install_script_path = File.expand_path("../bin/post-install.sh", __dir__)
10
+
11
+ if File.exist?(post_install_script_path)
12
+ # Make the script executable
13
+ File.chmod(0o755, post_install_script_path)
14
+
15
+ # Add Bun to PATH
16
+ ENV["PATH"] = "#{Dir.home}/.bun/bin:#{ENV.fetch('PATH', nil)}"
17
+
18
+ # Run the script
19
+ system("bash #{post_install_script_path}")
20
+
21
+ # Check if the script ran successfully
22
+ if $CHILD_STATUS.success?
23
+ puts "post installation completed successfully"
24
+ else
25
+ puts "post installation failed with exit code #{$CHILD_STATUS.exitstatus}"
26
+ end
27
+ else
28
+ puts "Warning: Could not find post install script at #{post_install_script_path}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "morio_bridge"
5
+ spec.version = "0.1.0"
6
+ spec.authors = ["r2g"]
7
+ spec.email = ["r2g.technology@gmail.com"]
8
+ spec.summary = "A simple Ruby gem with a post-install hook."
9
+ spec.description = "This gem demonstrates creating a Ruby gem with a post-install hook that installs Bun."
10
+ spec.homepage = "https://github.com/r2g/morio-bridge"
11
+ spec.license = "MIT"
12
+
13
+ # Specify which files to include in the gem
14
+ spec.files = Dir[
15
+ "lib/**/*",
16
+ "bin/**/*",
17
+ "README.md",
18
+ "LICENSE.txt",
19
+ "*.gemspec"
20
+ ]
21
+
22
+ spec.require_paths = ["lib"]
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
25
+
26
+ spec.metadata["rubygems_mfa_required"] = "true"
27
+ spec.required_ruby_version = ">= 3.4.3"
28
+ end
metadata ADDED
@@ -0,0 +1,46 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: morio_bridge
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - r2g
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: This gem demonstrates creating a Ruby gem with a post-install hook that
13
+ installs Bun.
14
+ email:
15
+ - r2g.technology@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/post-install.sh
21
+ - lib/morio_bridge.rb
22
+ - lib/rubygems_plugin.rb
23
+ - morio_bridge.gemspec
24
+ homepage: https://github.com/r2g/morio-bridge
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ rubygems_mfa_required: 'true'
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.4.3
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.6.7
44
+ specification_version: 4
45
+ summary: A simple Ruby gem with a post-install hook.
46
+ test_files: []