Class: Asherah::Session
- Inherits:
-
Object
- Object
- Asherah::Session
- Defined in:
- lib/asherah/session.rb
Constant Summary collapse
- DEFAULT_ASYNC_TIMEOUT_SECONDS =
Maximum wall time the async API will wait for the FFI callback to deliver a result before giving up. Without this bound, a hung tokio worker (or any callback-delivery race) would block the calling Ruby thread until the process exits — observed as 6-hour CI hangs on the round-trip tests. Override via ASHERAH_RUBY_ASYNC_TIMEOUT (seconds).
30- DEFAULT_CLOSE_DRAIN_SECONDS =
Maximum time #close will wait for in-flight async operations to drain before forcibly freeing the session. Independent of the per-call async timeout above.
5
Class Method Summary collapse
Instance Method Summary collapse
- #close ⇒ Object
- #closed? ⇒ Boolean
- #decrypt_bytes(json) ⇒ Object
-
#decrypt_bytes_async(json) ⇒ Object
True async decrypt — runs on Rust’s tokio runtime, does not block the Ruby thread.
- #encrypt_bytes(data) ⇒ Object
-
#encrypt_bytes_async(data) ⇒ Object
True async encrypt — runs on Rust’s tokio runtime, does not block the Ruby thread.
-
#initialize(pointer) ⇒ Session
constructor
A new instance of Session.
Constructor Details
#initialize(pointer) ⇒ Session
Returns a new instance of Session.
28 29 30 31 32 33 |
# File 'lib/asherah/session.rb', line 28 def initialize(pointer) raise Asherah::Error::GetSessionFailed, Native.last_error if pointer.null? @pointer = pointer @close_mu = Mutex.new @pending_ops = 0 end |
Class Method Details
.async_timeout_seconds ⇒ Object
20 21 22 23 24 25 26 |
# File 'lib/asherah/session.rb', line 20 def self.async_timeout_seconds val = ENV["ASHERAH_RUBY_ASYNC_TIMEOUT"] return DEFAULT_ASYNC_TIMEOUT_SECONDS if val.nil? || val.empty? Float(val) rescue ArgumentError, TypeError DEFAULT_ASYNC_TIMEOUT_SECONDS end |
Instance Method Details
#close ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/asherah/session.rb', line 129 def close ptr = @close_mu.synchronize do return if @pointer.null? # Wait for in-flight async operations before freeing, but bound # the wait — a wedged callback used to make this spin forever # (and silently wedge any process trying to shut down cleanly). deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + DEFAULT_CLOSE_DRAIN_SECONDS while @pending_ops > 0 && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline sleep 0.001 end if @pending_ops > 0 warn "asherah: closing session with #{@pending_ops} async operation(s) " \ "still in flight after #{DEFAULT_CLOSE_DRAIN_SECONDS}s drain" end p = @pointer @pointer = FFI::Pointer::NULL p end Native.asherah_session_free(ptr) end |
#closed? ⇒ Boolean
150 151 152 |
# File 'lib/asherah/session.rb', line 150 def closed? @pointer.null? end |
#decrypt_bytes(json) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/asherah/session.rb', line 53 def decrypt_bytes(json) raise ArgumentError, "json cannot be nil" if json.nil? raise Asherah::Error::DecryptFailed, "session closed" if @pointer.null? buf = thread_local_buffer status = Native.asherah_decrypt_from_json(@pointer, json, json.bytesize, buf.pointer) raise Asherah::Error::DecryptFailed, Native.last_error unless status.zero? # See encrypt_bytes — `ensure` guarantees the wipe runs even if # the read_bytes call somehow raises. begin buf[:data].read_bytes(buf[:len]) ensure Native.asherah_buffer_free(buf.pointer) end end |
#decrypt_bytes_async(json) ⇒ Object
True async decrypt — runs on Rust’s tokio runtime, does not block the Ruby thread.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/asherah/session.rb', line 102 def decrypt_bytes_async(json) raise ArgumentError, "json cannot be nil" if json.nil? raise Asherah::Error::DecryptFailed, "session closed" if @pointer.null? @close_mu.synchronize { @pending_ops += 1 } queue = Queue.new session = self callback = FFI::Function.new(:void, [:pointer, :pointer, :size_t, :string]) do |_ud, result_ptr, result_len, error| begin if error queue.push(Asherah::Error::DecryptFailed.new(error)) else queue.push(result_ptr.read_bytes(result_len)) end ensure session.send(:decrement_pending_ops) end end status = Native.asherah_decrypt_from_json_async(@pointer, json, json.bytesize, callback, nil) unless status.zero? @close_mu.synchronize { @pending_ops -= 1 } raise Asherah::Error::DecryptFailed, Native.last_error end result = await_async_result(queue, "decrypt_bytes_async") raise result if result.is_a?(Exception) result end |
#encrypt_bytes(data) ⇒ Object
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/asherah/session.rb', line 35 def encrypt_bytes(data) raise ArgumentError, "data cannot be nil" if data.nil? raise Asherah::Error::EncryptFailed, "session closed" if @pointer.null? buf = thread_local_buffer status = Native.asherah_encrypt_to_json(@pointer, data, data.bytesize, buf.pointer) raise Asherah::Error::EncryptFailed, Native.last_error unless status.zero? # `begin/ensure` so the FFI buffer is always freed (and its # plaintext bytes wiped via `asherah_buffer_free`'s zeroize) even # if `read_bytes` throws — without this the thread-local buffer # would leak the previous call's plaintext until the next # successful encrypt/decrypt on the same thread. begin buf[:data].read_bytes(buf[:len]) ensure Native.asherah_buffer_free(buf.pointer) end end |
#encrypt_bytes_async(data) ⇒ Object
True async encrypt — runs on Rust’s tokio runtime, does not block the Ruby thread. Returns the result; internally uses a Queue to wait for the tokio callback.
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/asherah/session.rb', line 70 def encrypt_bytes_async(data) raise ArgumentError, "data cannot be nil" if data.nil? raise Asherah::Error::EncryptFailed, "session closed" if @pointer.null? @close_mu.synchronize { @pending_ops += 1 } queue = Queue.new session = self callback = FFI::Function.new(:void, [:pointer, :pointer, :size_t, :string]) do |_ud, result_ptr, result_len, error| begin if error queue.push(Asherah::Error::EncryptFailed.new(error)) else queue.push(result_ptr.read_bytes(result_len)) end ensure session.send(:decrement_pending_ops) end end status = Native.asherah_encrypt_to_json_async(@pointer, data, data.bytesize, callback, nil) unless status.zero? @close_mu.synchronize { @pending_ops -= 1 } raise Asherah::Error::EncryptFailed, Native.last_error end # Bound the wait so a wedged callback can't block the calling # thread forever. We use Timeout.timeout (not Queue#pop(timeout:), # which only landed in Ruby 3.2) so the lib remains usable on the # 3.0/3.1 Ruby builds still in some CI/test images. result = await_async_result(queue, "encrypt_bytes_async") raise result if result.is_a?(Exception) result end |