Class: Asherah::Session

Inherits:
Object
  • Object
show all
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

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_secondsObject



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

#closeObject



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

Returns:

  • (Boolean)


150
151
152
# File 'lib/asherah/session.rb', line 150

def closed?
  @pointer.null?
end

#decrypt_bytes(json) ⇒ Object

Raises:

  • (ArgumentError)


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.

Raises:

  • (ArgumentError)


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

Raises:

  • (ArgumentError)


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.

Raises:

  • (ArgumentError)


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