ignis-collective 0.0.1

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.
Files changed (35) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +7 -0
  3. data/lib/ignis-collective.rb +9 -0
  4. data/lib/nvruby/collective/algorithms/double_binary_tree.rb +364 -0
  5. data/lib/nvruby/collective/algorithms/pipeliner.rb +222 -0
  6. data/lib/nvruby/collective/algorithms/reduction_ops.rb +168 -0
  7. data/lib/nvruby/collective/algorithms/ring.rb +421 -0
  8. data/lib/nvruby/collective/algorithms/topology_router.rb +284 -0
  9. data/lib/nvruby/collective/algorithms/tree.rb +291 -0
  10. data/lib/nvruby/collective/array_ops.rb +240 -0
  11. data/lib/nvruby/collective/communicator.rb +633 -0
  12. data/lib/nvruby/collective/communicator_healer.rb +276 -0
  13. data/lib/nvruby/collective/device_manager.rb +216 -0
  14. data/lib/nvruby/collective/dynamic_optimizer.rb +308 -0
  15. data/lib/nvruby/collective/health_monitor.rb +333 -0
  16. data/lib/nvruby/collective/net/nd_adapter.rb +450 -0
  17. data/lib/nvruby/collective/net/nd_bindings.rb +166 -0
  18. data/lib/nvruby/collective/net/rdma_transport.rb +366 -0
  19. data/lib/nvruby/collective/nvarray_adapter.rb +230 -0
  20. data/lib/nvruby/collective/p2p_bindings.rb +121 -0
  21. data/lib/nvruby/collective/resilient_transport.rb +296 -0
  22. data/lib/nvruby/collective/topology.rb +347 -0
  23. data/lib/nvruby/collective/transport/base.rb +138 -0
  24. data/lib/nvruby/collective/transport/host_staged_transport.rb +217 -0
  25. data/lib/nvruby/collective/transport/ipc_transport.rb +187 -0
  26. data/lib/nvruby/collective/transport/p2p_transport.rb +157 -0
  27. data/lib/nvruby/collective/transport/rdma_transports.rb +213 -0
  28. data/lib/nvruby/collective/transport/rio_transport.rb +405 -0
  29. data/lib/nvruby/collective/transport/tcp_transport.rb +290 -0
  30. data/lib/nvruby/collective/transport/vmm_ipc_structs.rb +189 -0
  31. data/lib/nvruby/collective/transport/vmm_ipc_transport.rb +266 -0
  32. data/lib/nvruby/collective/transport_selector.rb +200 -0
  33. data/lib/nvruby/collective/vmm_bindings.rb +212 -0
  34. data/lib/nvruby/collective.rb +156 -0
  35. metadata +92 -0
@@ -0,0 +1,450 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nd_bindings"
4
+
5
+ module Ignis
6
+ module Collective
7
+ module NetworkDirect
8
+ # RDMA Adapter wrapper for NetworkDirect
9
+ #
10
+ # Represents a single RDMA-capable NIC (e.g., Mellanox ConnectX-5)
11
+ # and provides methods for creating queue pairs and memory regions.
12
+ class Adapter
13
+ # @return [String] Adapter identifier
14
+ attr_reader :adapter_id
15
+
16
+ # @return [FFI::Pointer] Native adapter handle
17
+ attr_reader :handle
18
+
19
+ # @return [Bindings::ND2AdapterInfo] Adapter capabilities
20
+ attr_reader :info
21
+
22
+ # Initialize adapter wrapper
23
+ # @param handle [FFI::Pointer] Native IND2Adapter handle
24
+ # @param adapter_id [String] Adapter identifier
25
+ def initialize(handle, adapter_id)
26
+ @handle = handle
27
+ @adapter_id = adapter_id
28
+ @info = nil
29
+ @completion_queues = []
30
+ @queue_pairs = []
31
+ @memory_regions = []
32
+ end
33
+
34
+ # Query adapter capabilities
35
+ # @return [Hash] Adapter capabilities
36
+ def query
37
+ return @capabilities if @capabilities
38
+
39
+ @info = Bindings::ND2AdapterInfo.new
40
+ # In real implementation, call IND2Adapter::Query via COM interop
41
+
42
+ @capabilities = {
43
+ vendor_id: @info[:VendorId],
44
+ device_id: @info[:DeviceId],
45
+ max_registration_size: @info[:MaxRegistrationSize],
46
+ max_sge_count: @info[:MaxSgeCount],
47
+ max_send_queue_depth: @info[:MaxSendQueueDepth],
48
+ max_recv_queue_depth: @info[:MaxRecvQueueDepth],
49
+ max_cq_depth: @info[:MaxCqDepth],
50
+ max_inline_data: @info[:MaxInlineData],
51
+ max_outbound_read_limit: @info[:MaxOutboundReadLimit],
52
+ max_inbound_read_limit: @info[:MaxInboundReadLimit]
53
+ }
54
+ end
55
+
56
+ # Create a completion queue
57
+ # @param depth [Integer] Number of entries
58
+ # @return [CompletionQueue] New completion queue
59
+ def create_completion_queue(depth: 256)
60
+ cq = CompletionQueue.new(self, depth)
61
+ cq.create!
62
+ @completion_queues << cq
63
+ cq
64
+ end
65
+
66
+ # Create a queue pair
67
+ # @param send_cq [CompletionQueue] Send completion queue
68
+ # @param recv_cq [CompletionQueue] Receive completion queue
69
+ # @param send_depth [Integer] Send queue depth
70
+ # @param recv_depth [Integer] Receive queue depth
71
+ # @param sge_count [Integer] Max SGE per work request
72
+ # @return [QueuePair] New queue pair
73
+ def create_queue_pair(send_cq:, recv_cq:, send_depth: 64, recv_depth: 64, sge_count: 1)
74
+ qp = QueuePair.new(
75
+ adapter: self,
76
+ send_cq: send_cq,
77
+ recv_cq: recv_cq,
78
+ send_depth: send_depth,
79
+ recv_depth: recv_depth,
80
+ sge_count: sge_count
81
+ )
82
+ qp.create!
83
+ @queue_pairs << qp
84
+ qp
85
+ end
86
+
87
+ # Register a memory region for RDMA
88
+ # @param buffer [FFI::Pointer] Buffer to register
89
+ # @param size [Integer] Buffer size
90
+ # @param flags [Integer] Registration flags
91
+ # @return [MemoryRegion] Registered memory region
92
+ def register_memory(buffer, size, flags: 0)
93
+ mr = MemoryRegion.new(self, buffer, size, flags)
94
+ mr.register!
95
+ @memory_regions << mr
96
+ mr
97
+ end
98
+
99
+ # Create a connector for connection establishment
100
+ # @return [Connector] New connector
101
+ def create_connector
102
+ Connector.new(self)
103
+ end
104
+
105
+ # Release all resources
106
+ # @return [void]
107
+ def close!
108
+ @queue_pairs.each(&:close!)
109
+ @completion_queues.each(&:close!)
110
+ @memory_regions.each(&:deregister!)
111
+
112
+ @queue_pairs.clear
113
+ @completion_queues.clear
114
+ @memory_regions.clear
115
+
116
+ # Release native handle
117
+ # In real implementation: IND2Adapter::Release
118
+ @handle = nil
119
+ end
120
+ end
121
+
122
+ # Completion Queue for RDMA operation completions
123
+ class CompletionQueue
124
+ # @return [Adapter] Parent adapter
125
+ attr_reader :adapter
126
+
127
+ # @return [Integer] Queue depth
128
+ attr_reader :depth
129
+
130
+ # @return [FFI::Pointer] Native CQ handle
131
+ attr_reader :handle
132
+
133
+ def initialize(adapter, depth)
134
+ @adapter = adapter
135
+ @depth = depth
136
+ @handle = nil
137
+ @created = false
138
+ end
139
+
140
+ # Create the completion queue
141
+ # @return [void]
142
+ def create!
143
+ return if @created
144
+
145
+ # In real implementation: IND2Adapter::CreateCompletionQueue
146
+ @handle = FFI::Pointer.new(:void, 0x12345678) # Placeholder
147
+ @created = true
148
+ end
149
+
150
+ # Poll for completions
151
+ # @param max_results [Integer] Max completions to retrieve
152
+ # @return [Array<Hash>] Completion results
153
+ def poll(max_results: 16)
154
+ return [] unless @created
155
+
156
+ # Allocate result buffer
157
+ results_ptr = FFI::MemoryPointer.new(Bindings::ND2Result, max_results)
158
+ count_ptr = FFI::MemoryPointer.new(:uint32)
159
+
160
+ # In real implementation: IND2CompletionQueue::GetResults
161
+ # Returns array of completion results
162
+
163
+ # Parse results
164
+ results = []
165
+ count = count_ptr.read_uint32
166
+ count.times do |i|
167
+ result = Bindings::ND2Result.new(results_ptr + i * Bindings::ND2Result.size)
168
+ results << {
169
+ status: result[:Status],
170
+ bytes_transferred: result[:BytesTransferred],
171
+ context: result[:RequestContext],
172
+ type: result[:RequestType]
173
+ }
174
+ end
175
+
176
+ results
177
+ end
178
+
179
+ # Wait for completion with timeout
180
+ # @param timeout_ms [Integer] Timeout in milliseconds
181
+ # @return [Boolean] True if completion available
182
+ def wait(timeout_ms: 1000)
183
+ return false unless @created
184
+
185
+ # In real implementation: IND2CompletionQueue::Notify with INFINITE or timeout
186
+ true
187
+ end
188
+
189
+ # Close the completion queue
190
+ # @return [void]
191
+ def close!
192
+ return unless @created
193
+
194
+ # In real implementation: IND2CompletionQueue::Release
195
+ @handle = nil
196
+ @created = false
197
+ end
198
+ end
199
+
200
+ # Queue Pair for RDMA send/receive operations
201
+ class QueuePair
202
+ # States
203
+ STATE_INIT = :init
204
+ STATE_READY = :ready
205
+ STATE_CONNECTED = :connected
206
+ STATE_ERROR = :error
207
+
208
+ # @return [Adapter] Parent adapter
209
+ attr_reader :adapter
210
+
211
+ # @return [Symbol] Current state
212
+ attr_reader :state
213
+
214
+ # @return [FFI::Pointer] Native QP handle
215
+ attr_reader :handle
216
+
217
+ def initialize(adapter:, send_cq:, recv_cq:, send_depth:, recv_depth:, sge_count:)
218
+ @adapter = adapter
219
+ @send_cq = send_cq
220
+ @recv_cq = recv_cq
221
+ @send_depth = send_depth
222
+ @recv_depth = recv_depth
223
+ @sge_count = sge_count
224
+ @handle = nil
225
+ @state = STATE_INIT
226
+ end
227
+
228
+ # Create the queue pair
229
+ # @return [void]
230
+ def create!
231
+ return if @state != STATE_INIT
232
+
233
+ # In real implementation: IND2Adapter::CreateQueuePair
234
+ @handle = FFI::Pointer.new(:void, 0x87654321) # Placeholder
235
+ @state = STATE_READY
236
+ end
237
+
238
+ # Post a send work request
239
+ # @param sge_list [Array<Hash>] Scatter-gather entries
240
+ # @param context [FFI::Pointer] User context
241
+ # @param flags [Integer] Send flags (inline, signaled, etc.)
242
+ # @return [void]
243
+ def post_send(sge_list:, context: nil, flags: 0)
244
+ raise RDMAError, "QP not ready" unless @state == STATE_CONNECTED
245
+
246
+ # Build SGE array
247
+ sge_count = sge_list.size
248
+ sge_ptr = FFI::MemoryPointer.new(Bindings::ND2Sge, sge_count)
249
+
250
+ sge_list.each_with_index do |sge, i|
251
+ entry = Bindings::ND2Sge.new(sge_ptr + i * Bindings::ND2Sge.size)
252
+ entry[:Buffer] = sge[:buffer]
253
+ entry[:BufferLength] = sge[:length]
254
+ entry[:MemoryRegionToken] = sge[:token]
255
+ end
256
+
257
+ # In real implementation: IND2QueuePair::Send
258
+ end
259
+
260
+ # Post a receive work request
261
+ # @param sge_list [Array<Hash>] Scatter-gather entries
262
+ # @param context [FFI::Pointer] User context
263
+ # @return [void]
264
+ def post_receive(sge_list:, context: nil)
265
+ raise RDMAError, "QP not ready" unless @state == STATE_READY || @state == STATE_CONNECTED
266
+
267
+ # Build SGE array (similar to post_send)
268
+ # In real implementation: IND2QueuePair::Receive
269
+ end
270
+
271
+ # RDMA Write operation (one-sided)
272
+ # @param remote_address [Integer] Remote buffer address
273
+ # @param remote_token [Integer] Remote memory token
274
+ # @param sge_list [Array<Hash>] Local scatter-gather entries
275
+ # @param context [FFI::Pointer] User context
276
+ # @return [void]
277
+ def rdma_write(remote_address:, remote_token:, sge_list:, context: nil)
278
+ raise RDMAError, "QP not connected" unless @state == STATE_CONNECTED
279
+
280
+ # In real implementation: IND2QueuePair::Write
281
+ end
282
+
283
+ # RDMA Read operation (one-sided)
284
+ # @param remote_address [Integer] Remote buffer address
285
+ # @param remote_token [Integer] Remote memory token
286
+ # @param sge_list [Array<Hash>] Local scatter-gather entries
287
+ # @param context [FFI::Pointer] User context
288
+ # @return [void]
289
+ def rdma_read(remote_address:, remote_token:, sge_list:, context: nil)
290
+ raise RDMAError, "QP not connected" unless @state == STATE_CONNECTED
291
+
292
+ # In real implementation: IND2QueuePair::Read
293
+ end
294
+
295
+ # Transition to connected state
296
+ # @return [void]
297
+ def set_connected!
298
+ @state = STATE_CONNECTED
299
+ end
300
+
301
+ # Close the queue pair
302
+ # @return [void]
303
+ def close!
304
+ # In real implementation: IND2QueuePair::Release
305
+ @handle = nil
306
+ @state = STATE_ERROR
307
+ end
308
+ end
309
+
310
+ # Memory Region for RDMA registration
311
+ class MemoryRegion
312
+ # @return [FFI::Pointer] Buffer pointer
313
+ attr_reader :buffer
314
+
315
+ # @return [Integer] Buffer size
316
+ attr_reader :size
317
+
318
+ # @return [Integer] Memory region token (for remote access)
319
+ attr_reader :token
320
+
321
+ # @return [FFI::Pointer] Native MR handle
322
+ attr_reader :handle
323
+
324
+ def initialize(adapter, buffer, size, flags)
325
+ @adapter = adapter
326
+ @buffer = buffer
327
+ @size = size
328
+ @flags = flags
329
+ @handle = nil
330
+ @token = nil
331
+ @registered = false
332
+ end
333
+
334
+ # Register the memory region
335
+ # @return [void]
336
+ def register!
337
+ return if @registered
338
+
339
+ # In real implementation: IND2Adapter::CreateMemoryRegion + Register
340
+ @handle = FFI::Pointer.new(:void, 0xABCDEF00) # Placeholder
341
+ @token = rand(0xFFFFFFFF) # Would be from GetRemoteToken
342
+ @registered = true
343
+ end
344
+
345
+ # Get remote access token for RDMA write/read
346
+ # @return [Hash] Remote access info
347
+ def remote_access_info
348
+ raise RDMAError, "Memory not registered" unless @registered
349
+
350
+ {
351
+ address: @buffer.address,
352
+ token: @token,
353
+ size: @size
354
+ }
355
+ end
356
+
357
+ # Deregister the memory region
358
+ # @return [void]
359
+ def deregister!
360
+ return unless @registered
361
+
362
+ # In real implementation: IND2MemoryRegion::Deregister + Release
363
+ @handle = nil
364
+ @token = nil
365
+ @registered = false
366
+ end
367
+ end
368
+
369
+ # Connector for RDMA connection establishment
370
+ class Connector
371
+ # @return [Adapter] Parent adapter
372
+ attr_reader :adapter
373
+
374
+ # @return [FFI::Pointer] Native connector handle
375
+ attr_reader :handle
376
+
377
+ def initialize(adapter)
378
+ @adapter = adapter
379
+ @handle = nil
380
+ @bound = false
381
+ @connected = false
382
+ end
383
+
384
+ # Bind to local address
385
+ # @param address [String] Local IP address
386
+ # @param port [Integer] Local port
387
+ # @return [void]
388
+ def bind(address:, port:)
389
+ return if @bound
390
+
391
+ # In real implementation: IND2Connector::Bind
392
+ @local_address = address
393
+ @local_port = port
394
+ @bound = true
395
+ end
396
+
397
+ # Connect to remote peer (client side)
398
+ # @param qp [QueuePair] Queue pair to connect
399
+ # @param remote_address [String] Remote IP address
400
+ # @param remote_port [Integer] Remote port
401
+ # @param private_data [String, nil] Connection private data
402
+ # @return [void]
403
+ def connect(qp:, remote_address:, remote_port:, private_data: nil)
404
+ raise RDMAError, "Not bound" unless @bound
405
+
406
+ # In real implementation: IND2Connector::Connect
407
+ # Then poll for completion
408
+ # Then CompleteConnect
409
+
410
+ @connected = true
411
+ qp.set_connected!
412
+ end
413
+
414
+ # Accept connection (server side)
415
+ # @param qp [QueuePair] Queue pair for the connection
416
+ # @param private_data [String, nil] Response private data
417
+ # @return [void]
418
+ def accept(qp:, private_data: nil)
419
+ raise RDMAError, "Not bound" unless @bound
420
+
421
+ # In real implementation:
422
+ # 1. Listen for incoming connection
423
+ # 2. GetConnectionRequest
424
+ # 3. Accept with QP
425
+
426
+ @connected = true
427
+ qp.set_connected!
428
+ end
429
+
430
+ # Disconnect
431
+ # @return [void]
432
+ def disconnect!
433
+ return unless @connected
434
+
435
+ # In real implementation: IND2Connector::Disconnect
436
+ @connected = false
437
+ end
438
+
439
+ # Close the connector
440
+ # @return [void]
441
+ def close!
442
+ disconnect! if @connected
443
+ # Release handle
444
+ @handle = nil
445
+ @bound = false
446
+ end
447
+ end
448
+ end
449
+ end
450
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Ignis
6
+ module Collective
7
+ module NetworkDirect
8
+ # Windows NetworkDirect SPI v2 FFI Bindings
9
+ #
10
+ # NetworkDirect is **Windows-native** RDMA API supporting:
11
+ # - InfiniBand
12
+ # - RoCE (RDMA over Converged Ethernet)
13
+ # - iWARP
14
+ #
15
+ # Requires: Mellanox ConnectX NICs with WinOF-2 drivers
16
+ #
17
+ # Key interfaces (COM-based):
18
+ # - IND2Provider: Service provider, opens adapters
19
+ # - IND2Adapter: Hardware adapter, creates queues
20
+ # - IND2CompletionQueue: RDMA completion notifications
21
+ # - IND2QueuePair: Send/Receive queue pair for I/O
22
+ # - IND2MemoryRegion: Registered RDMA memory
23
+ # - IND2Connector: Connection establishment
24
+ module Bindings
25
+ extend FFI::Library
26
+
27
+ # NetworkDirect status codes
28
+ ND_SUCCESS = 0
29
+ ND_TIMEOUT = 0x80070102
30
+ ND_PENDING = 0x80000005
31
+ ND_BUFFER_OVERFLOW = 0x8007006F
32
+ ND_DEVICE_NOT_READY = 0x80070015
33
+ ND_NO_MEMORY = 0x8007000E
34
+ ND_CONNECTION_REFUSED = 0x8007107D
35
+ ND_CONNECTION_ABORTED = 0x80070453
36
+ ND_CONNECTION_INVALID = 0x800710CD
37
+ ND_NOT_SUPPORTED = 0x80070032
38
+
39
+ # Queue pair types
40
+ ND_QP_TYPE_SEND_RECV = 0
41
+ ND_QP_TYPE_RDMA = 1
42
+
43
+ # Work completion status
44
+ ND_CQ_SUCCESS = 0
45
+ ND_CQ_FLUSHED = 1
46
+ ND_CQ_LOCAL_ERROR = 2
47
+ ND_CQ_TRANSPORT_ERROR = 3
48
+ ND_CQ_RNR_ERROR = 4
49
+
50
+ # ND2_RESULT structure for completion queue
51
+ class ND2Result < FFI::Struct
52
+ layout :Status, :uint32, # Completion status
53
+ :BytesTransferred, :uint32,
54
+ :QueuePairContext, :pointer,
55
+ :RequestContext, :pointer,
56
+ :RequestType, :uint32
57
+ end
58
+
59
+ # ND2_ADAPTER_INFO structure
60
+ class ND2AdapterInfo < FFI::Struct
61
+ layout :InfoVersion, :uint32,
62
+ :VendorId, :uint32,
63
+ :DeviceId, :uint32,
64
+ :AdapterId, :uint64,
65
+ :MaxRegistrationSize, :size_t,
66
+ :MaxWindowSize, :size_t,
67
+ :MaxSgeCount, :uint32,
68
+ :MaxReadQueueDepth, :uint32,
69
+ :MaxRecvQueueDepth, :uint32,
70
+ :MaxSendQueueDepth, :uint32,
71
+ :MaxCqDepth, :uint32,
72
+ :MaxInlineData, :uint32,
73
+ :MaxOutboundReadLimit, :uint32,
74
+ :MaxInboundReadLimit, :uint32
75
+ end
76
+
77
+ # ND2_SGE (Scatter-Gather Entry)
78
+ class ND2Sge < FFI::Struct
79
+ layout :Buffer, :pointer,
80
+ :BufferLength, :uint32,
81
+ :MemoryRegionToken, :uint32
82
+ end
83
+
84
+ @loaded = false
85
+
86
+ def self.ensure_loaded!
87
+ return if @loaded
88
+
89
+ begin
90
+ # NetworkDirect provider DLL (from Mellanox WinOF-2 or similar)
91
+ # Standard locations for RDMA providers
92
+ ffi_lib [
93
+ "nd",
94
+ "NetworkDirect",
95
+ "mlx5nd", # Mellanox ConnectX-5+
96
+ "mlx4nd" # Older Mellanox
97
+ ]
98
+ attach_nd_functions!
99
+ @loaded = true
100
+ rescue FFI::NotFoundError => e
101
+ # Not an error if hardware not present
102
+ @loaded = false
103
+ @load_error = e.message
104
+ end
105
+ end
106
+
107
+ def self.available?
108
+ ensure_loaded!
109
+ @loaded
110
+ end
111
+
112
+ def self.load_error
113
+ @load_error
114
+ end
115
+
116
+ def self.attach_nd_functions!
117
+ # Provider enumeration
118
+ attach_function :NdOpenAdapter, [
119
+ :pointer, # const ND2_ADAPTER_ADDRESS* pAddress
120
+ :uint32, # DWORD cbAddressLength
121
+ :pointer # IND2Adapter** ppAdapter (output)
122
+ ], :int32
123
+
124
+ # Query adapters
125
+ attach_function :NdQueryAddressList, [
126
+ :uint32, # DWORD Flags
127
+ :pointer, # ND2_ADAPTER_ADDRESS* pAddressList
128
+ :pointer # DWORD* pcbAddressList (in/out)
129
+ ], :int32
130
+
131
+ # Provider initialization
132
+ attach_function :NdStartup, [:uint16], :int32 # Version
133
+ attach_function :NdCleanup, [], :int32
134
+ end
135
+
136
+ # Check status and raise on error
137
+ def self.check_status!(status, context = "NetworkDirect operation")
138
+ return if status == ND_SUCCESS
139
+
140
+ error_name = case status
141
+ when ND_TIMEOUT then "ND_TIMEOUT"
142
+ when ND_PENDING then "ND_PENDING"
143
+ when ND_NO_MEMORY then "ND_NO_MEMORY"
144
+ when ND_DEVICE_NOT_READY then "ND_DEVICE_NOT_READY"
145
+ when ND_CONNECTION_REFUSED then "ND_CONNECTION_REFUSED"
146
+ when ND_CONNECTION_ABORTED then "ND_CONNECTION_ABORTED"
147
+ when ND_NOT_SUPPORTED then "ND_NOT_SUPPORTED"
148
+ else "UNKNOWN_ERROR"
149
+ end
150
+
151
+ raise RDMAError.new("#{context}: #{error_name} (0x#{status.to_s(16)})", code: status)
152
+ end
153
+ end
154
+
155
+ # Custom RDMA error class
156
+ class RDMAError < StandardError
157
+ attr_reader :code
158
+
159
+ def initialize(message, code: nil)
160
+ super(message)
161
+ @code = code
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end