Class: Tina4::SessionHandlers::MongoWireClient

Inherits:
Object
  • Object
show all
Defined in:
lib/tina4/session_handlers/mongo_wire_client.rb

Overview

Zero-dependency synchronous MongoDB client speaking the wire protocol (OP_MSG, opcode 2013) over a TCP socket, with a minimal BSON codec.

WHY IT EXISTS (session_contract.json #6, ADR-0024). Python, PHP and Node have always shipped a raw OP_MSG fallback for the mongodb SESSION backend; Ruby alone hard-required the mongo gem and RAISED without it. The same .env therefore worked in three frameworks and failed in the fourth, which breaks the zero-dependency promise ASYMMETRICALLY - worse than breaking it consistently, because nothing about the configuration hints at it. This is the missing fourth transport, ported from the proven Python master (tina4_python/session_handlers/mongodb_handler.py) rather than invented.

It is deliberately NOT a general Mongo driver. It implements exactly the four operations the session handler performs - find_one by _id, upsert by _id, delete_one by _id, and delete_many for gc - and it mirrors the SHAPE of the mongo gem's Collection for those four so the handler's read, write, destroy and gc bodies are IDENTICAL on both transports. That is not cosmetic: a branch inside each operation is a branch that can be wrong on one path only, and this fallback would then ship untested behind a condition nobody exercises.

HARD-WON DETAILS, preserved from the three siblings:

* The COMMAND NAME comes FIRST in the document and `$db` LAST. Reversed,
the server reads `$db` as the command name and answers CommandNotFound.
* BSON is little-endian throughout, string lengths are BYTE counts (not
character counts), and a Time is encoded as BSON UTC datetime (0x09,
int64 milliseconds) so a document written here is byte-identical in
shape to one the gem writes.
* An OP_MSG reply can arrive split across TCP segments, so the body is
read by its exact declared byte count rather than one recv().

NO NETWORK I/O IN A CONSTRUCTOR (ADR-0021, session_contract.json #4): the socket is opened on the FIRST command, inside the log-loud-and-degrade policy, never here.

Constant Summary collapse

OP_MSG =
2013
TYPE_DOUBLE =

BSON element type bytes, named so the codec reads as the spec does.

0x01
TYPE_STRING =
0x02
TYPE_DOCUMENT =
0x03
TYPE_ARRAY =
0x04
TYPE_BINARY =
0x05
TYPE_OBJECT_ID =
0x07
TYPE_BOOLEAN =
0x08
TYPE_DATETIME =
0x09
TYPE_NULL =
0x0A
TYPE_INT32 =
0x10
TYPE_TIMESTAMP =
0x11
TYPE_INT64 =
0x12
INT32_MIN =
-2_147_483_648
INT32_MAX =
2_147_483_647

Instance Method Summary collapse

Constructor Details

#initialize(host:, port:, database:, collection:, timeout: 10) ⇒ MongoWireClient

Returns a new instance of MongoWireClient.



66
67
68
69
70
71
72
73
74
# File 'lib/tina4/session_handlers/mongo_wire_client.rb', line 66

def initialize(host:, port:, database:, collection:, timeout: 10)
  @host = host
  @port = port
  @database = database
  @collection = collection
  @timeout = timeout
  @socket = nil
  @request_id = 0
end

Instance Method Details

#closeObject



104
105
106
107
108
109
110
# File 'lib/tina4/session_handlers/mongo_wire_client.rb', line 104

def close
  @socket&.close
rescue StandardError
  nil # already closed / never opened - nothing to do
ensure
  @socket = nil
end

#delete_many(filter) ⇒ Object



100
101
102
# File 'lib/tina4/session_handlers/mongo_wire_client.rb', line 100

def delete_many(filter)
  delete(filter, 0)
end

#delete_one(filter) ⇒ Object



96
97
98
# File 'lib/tina4/session_handlers/mongo_wire_client.rb', line 96

def delete_one(filter)
  delete(filter, 1)
end

#find(filter) ⇒ Object

Documents matching filter, capped at one.

Returns an ARRAY so the caller can write find(...).first exactly as it does against the gem's Collection, which answers a lazy view. One shared call site, two transports.



81
82
83
84
# File 'lib/tina4/session_handlers/mongo_wire_client.rb', line 81

def find(filter)
  reply = command("find" => @collection, "filter" => filter, "limit" => 1, "$db" => @database)
  reply.dig("cursor", "firstBatch") || []
end

#update_one(filter, update, upsert: false) ⇒ Object

Apply update (an operator document such as {"$set" => {...}}, or a full replacement) to the single document matching filter.



88
89
90
91
92
93
94
# File 'lib/tina4/session_handlers/mongo_wire_client.rb', line 88

def update_one(filter, update, upsert: false)
  command(
    "update" => @collection,
    "updates" => [{ "q" => filter, "u" => update, "upsert" => upsert }],
    "$db" => @database
  )
end