neo4j-ruby-driver 6.2.1.beta.3 → 6.2.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.
- checksums.yaml +4 -4
- data/lib/neo4j/driver/session.rb +24 -14
- data/lib/neo4j/driver/transaction.rb +13 -332
- data/lib/neo4j/driver/unmanaged_transaction.rb +348 -0
- data/lib/neo4j/driver/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 701bd350949810304707654ca39de518eda02faeda8b6d1c9575d14a3f5ac703
|
|
4
|
+
data.tar.gz: 7920f8aa3f11ded084dda941bf7de0882e7858c7b99d69168b816e2ab6b65e44
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9afb8c944e1661dcc0f5d6f4573d6e797bc218f8f823ee5304c1e7d4ae01bd85fe6804e055f1dd58b41732cbd852a2bb8fd4128f5127c8a3644c73c705903af3
|
|
7
|
+
data.tar.gz: bfd5dd64dd2cd2709409daeb8f3df67b641119c24683f4e6e2022db3fb3de335bc83c8f3ad04c6cefbcdb701121bc06db6198549a39237346f26fc9939fe2fd5
|
data/lib/neo4j/driver/session.rb
CHANGED
|
@@ -203,10 +203,11 @@ module Neo4j
|
|
|
203
203
|
result = yield @transaction
|
|
204
204
|
# Explicit-block transactions default to rollback; user must call
|
|
205
205
|
# tx.commit to persist changes (matches Java driver semantics).
|
|
206
|
-
|
|
206
|
+
# #close rolls back iff still open (a no-op once committed).
|
|
207
|
+
@transaction.close
|
|
207
208
|
result
|
|
208
209
|
rescue StandardError => e
|
|
209
|
-
@transaction.
|
|
210
|
+
@transaction.close
|
|
210
211
|
raise e
|
|
211
212
|
ensure
|
|
212
213
|
@transaction = nil
|
|
@@ -540,13 +541,22 @@ module Neo4j
|
|
|
540
541
|
telemetry_ack: on_telemetry_ack)
|
|
541
542
|
|
|
542
543
|
begin
|
|
543
|
-
|
|
544
|
-
|
|
544
|
+
# Managed functions yield a run-only context; the driver owns
|
|
545
|
+
# commit/rollback on the underlying UnmanagedTransaction (@transaction).
|
|
546
|
+
# The block can only #run — it can't close the tx — and any run
|
|
547
|
+
# failure raises (→ rescue), so a clean return always leaves it open:
|
|
548
|
+
# commit unconditionally.
|
|
549
|
+
result = yield Transaction.new(@transaction)
|
|
550
|
+
@transaction.commit
|
|
545
551
|
result
|
|
546
552
|
rescue StandardError => e
|
|
547
|
-
@transaction.
|
|
553
|
+
@transaction.close
|
|
548
554
|
raise e
|
|
549
555
|
ensure
|
|
556
|
+
# A non-local exit from the work block (return/throw/break through an
|
|
557
|
+
# outer iterator) skips both the commit and the rescue; #close rolls
|
|
558
|
+
# back the still-open tx so it and its connection lease aren't leaked.
|
|
559
|
+
@transaction.close
|
|
550
560
|
@transaction = nil
|
|
551
561
|
end
|
|
552
562
|
end
|
|
@@ -562,15 +572,15 @@ module Neo4j
|
|
|
562
572
|
# commit-time update_bookmarks reports it as `previous`).
|
|
563
573
|
bookmarks = current_bookmarks_for_extra
|
|
564
574
|
tx_options = tx_options.merge(database: begin_db).compact
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
575
|
+
UnmanagedTransaction.new(connection, self, bookmarks, tx_options, telemetry_api: telemetry_api,
|
|
576
|
+
telemetry_ack: telemetry_ack,
|
|
577
|
+
# executeQuery's session reports telemetry api 3 (DRIVER_EXECUTE_QUERY);
|
|
578
|
+
# only that path pipelines BEGIN + RUN + PULL (Optimization:ExecuteQueryPipelining).
|
|
579
|
+
pipelined: @options[:telemetry_api] == 3,
|
|
580
|
+
on_begin: method(:cache_home_db_from),
|
|
581
|
+
on_release: lambda {
|
|
582
|
+
@connection_provider.release(connection)
|
|
583
|
+
})
|
|
574
584
|
end
|
|
575
585
|
end
|
|
576
586
|
end
|
|
@@ -2,342 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Neo4j
|
|
4
4
|
module Driver
|
|
5
|
-
#
|
|
5
|
+
# The transaction context yielded to execute_read / execute_write work
|
|
6
|
+
# blocks. It deliberately exposes only #run: the driver owns the lifecycle
|
|
7
|
+
# of a managed transaction (auto-commit on clean return, rollback + retry
|
|
8
|
+
# on failure), so the block must not commit, roll back, or close it.
|
|
9
|
+
#
|
|
10
|
+
# This is Neo4j::Driver::Transaction on both flavours — the JRuby flavour
|
|
11
|
+
# binds the constant to Java's DelegatingTransactionContext — so downstream
|
|
12
|
+
# code prepending onto Neo4j::Driver::Transaction targets the managed
|
|
13
|
+
# context consistently. Explicit transactions (session.begin_transaction)
|
|
14
|
+
# use UnmanagedTransaction, which keeps commit/rollback/close.
|
|
6
15
|
class Transaction
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def initialize(connection, session, bookmarks = [], options = {}, telemetry_api: nil, telemetry_ack: nil,
|
|
10
|
-
pipelined: false, on_begin: nil, on_release: nil)
|
|
11
|
-
@connection = connection
|
|
12
|
-
@session = session
|
|
13
|
-
@options = options
|
|
14
|
-
# executeQuery pipelines BEGIN + RUN + PULL (Optimization:ExecuteQueryPipelining):
|
|
15
|
-
# BEGIN's reply is read only after the first RUN+PULL are flushed, not eagerly.
|
|
16
|
-
@pipelined = pipelined
|
|
17
|
-
@on_begin = on_begin # called with the BEGIN reply (home-db cache update)
|
|
18
|
-
@on_release = on_release # called once when the connection is no longer needed
|
|
19
|
-
@open = true
|
|
20
|
-
@committed = false
|
|
21
|
-
@rolled_back = false
|
|
22
|
-
@failed = false
|
|
23
|
-
@terminating_error = nil # the classified error that terminated this tx (a RUN/commit failure)
|
|
24
|
-
@current_result = nil
|
|
25
|
-
# Every result opened in this tx, in order. Bolt lets multiple stay open
|
|
26
|
-
# and streaming concurrently (qid multiplexing); a new RUN no longer
|
|
27
|
-
# force-buffers the previous one, so we track them all here to discard
|
|
28
|
-
# any still-open at commit/rollback and to spot a mid-stream failure.
|
|
29
|
-
@open_results = []
|
|
30
|
-
|
|
31
|
-
# Begin the transaction
|
|
32
|
-
# Drop blank values so the serialised BEGIN map matches what
|
|
33
|
-
# testkit's stub scripts expect (e.g. `BEGIN {"db": "adb"}`,
|
|
34
|
-
# not `BEGIN {"db": "adb", "tx_metadata": {}}`).
|
|
35
|
-
begin_extra = {
|
|
36
|
-
bookmarks: bookmarks,
|
|
37
|
-
db: options[:database],
|
|
38
|
-
mode: options[:access_mode],
|
|
39
|
-
tx_timeout: options[:timeout],
|
|
40
|
-
tx_metadata: options[:metadata],
|
|
41
|
-
imp_user: options[:impersonated_user]
|
|
42
|
-
}
|
|
43
|
-
begin_extra.reject!(&Internal::Extras::BLANK)
|
|
44
|
-
|
|
45
|
-
# TELEMETRY (api = 0 managed / 1 explicit / 3 executeQuery) pipelined
|
|
46
|
-
# ahead of BEGIN when the server opted in and the driver didn't disable
|
|
47
|
-
# it; its SUCCESS is read first (see #ack_begin!).
|
|
48
|
-
@telemetry_sent = @connection.telemetry(telemetry_api, disabled: options[:telemetry_disabled])
|
|
49
|
-
@telemetry_ack = telemetry_ack
|
|
50
|
-
# Session-level NotificationsConfig rides on BEGIN (5.2+); the tx's own
|
|
51
|
-
# RUNs carry none. nil / pre-5.2 => no notification keys on the wire.
|
|
52
|
-
@connection.send_message(@connection.protocol.build_begin(begin_extra,
|
|
53
|
-
notification_config: options[:notification_config]))
|
|
54
|
-
@connection.flush
|
|
55
|
-
|
|
56
|
-
# Non-pipelined: read BEGIN's reply now, a plain round-trip (unchanged).
|
|
57
|
-
# Pipelined (executeQuery): defer it so the first #run can flush RUN+PULL
|
|
58
|
-
# before we block — a pipelining server withholds BEGIN's SUCCESS until it
|
|
59
|
-
# has all three. #ack_begin! drains it after that flush.
|
|
60
|
-
@begin_acked = false
|
|
61
|
-
ack_begin! unless @pipelined
|
|
62
|
-
rescue Exceptions::Neo4jException => e
|
|
63
|
-
# Classify first so the auth-token manager is notified and the
|
|
64
|
-
# connection is flagged for discard on an auth failure (the server
|
|
65
|
-
# closes it). BEGIN failed → server is in FAILED state; RESET to
|
|
66
|
-
# make it reusable, unless it's being discarded (a security
|
|
67
|
-
# failure: the server closes it and RESET would just error).
|
|
68
|
-
classified = @connection.classify_failure(e)
|
|
69
|
-
@connection.reset! unless @connection.auth_failed
|
|
70
|
-
@open = false
|
|
71
|
-
release_connection
|
|
72
|
-
raise classified
|
|
73
|
-
rescue StandardError
|
|
74
|
-
# Transport-level failure (IO/socket). RESET will likely fail
|
|
75
|
-
# too on a dead connection, but release the lease so it doesn't
|
|
76
|
-
# leak; pool reuse will surface the breakage to the next caller.
|
|
77
|
-
@open = false
|
|
78
|
-
release_connection
|
|
79
|
-
raise
|
|
16
|
+
def initialize(transaction)
|
|
17
|
+
@transaction = transaction
|
|
80
18
|
end
|
|
81
19
|
|
|
82
20
|
def run(query, **parameters)
|
|
83
|
-
|
|
84
|
-
# A server failure terminated this transaction; further work is
|
|
85
|
-
# rejected locally (no wire traffic) as a TransactionTerminatedException
|
|
86
|
-
# — a ClientException subclass, matching the Java driver. Covers both a
|
|
87
|
-
# failed RUN (sets @failed) and a result that failed mid-stream during
|
|
88
|
-
# the user's own iteration (@current_result.failed?). Message wording
|
|
89
|
-
# stays "rolled back" to match the Java flavor (a shared integration spec
|
|
90
|
-
# asserts it on both impls); only the exception class becomes specific.
|
|
91
|
-
if terminated?
|
|
92
|
-
raise Exceptions::TransactionTerminatedException,
|
|
93
|
-
'Cannot run more queries in this transaction, it has been rolled back'
|
|
94
|
-
end
|
|
95
|
-
unless @open
|
|
96
|
-
# Mirror the Java/JRuby messages so the closed-state reason
|
|
97
|
-
# (committed vs rolled back) is reported the same on both impls.
|
|
98
|
-
raise Exceptions::ClientException,
|
|
99
|
-
"Cannot run more queries in this transaction, it has been #{@committed ? 'committed' : 'rolled back'}"
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
# A new query becomes current; the previous result must now name its qid
|
|
103
|
-
# explicitly on further PULL/DISCARD (the server defaults them to the last
|
|
104
|
-
# opened query). We keep it open and streaming rather than buffering it —
|
|
105
|
-
# that's the qid multiplexing the nested-result tests exercise.
|
|
106
|
-
@current_result&.demote!
|
|
107
|
-
|
|
108
|
-
fetch_size = effective_fetch_size
|
|
109
|
-
|
|
110
|
-
# send/flush are inside the begin block so a transport-level
|
|
111
|
-
# failure surfacing on fetch_response still sets @failed and
|
|
112
|
-
# goes through classify_failure. Connection#send_message and
|
|
113
|
-
# #flush both defer peer-closed errors so a buffered server
|
|
114
|
-
# FAILURE is read before fetch_response can raise its own
|
|
115
|
-
# EOF-driven ServiceUnavailableException — JRuby surfaces
|
|
116
|
-
# EPIPE eagerly, MRI tends to defer it naturally. Without
|
|
117
|
-
# this rescue placement, session.close's subsequent rollback
|
|
118
|
-
# path takes the ROLLBACK-message branch (rather than
|
|
119
|
-
# rollback_via_reset) and re-fails on the dead connection.
|
|
120
|
-
buffer = Bolt::RecordBuffer.new(fetch_size: fetch_size)
|
|
121
|
-
handler = Bolt::StreamHandler.new(buffer)
|
|
122
|
-
run_response =
|
|
123
|
-
begin
|
|
124
|
-
@connection.send_message(@connection.protocol.build_run(query, parameters, {}))
|
|
125
|
-
@connection.send_message(@connection.protocol.build_pull(n: fetch_size), handler)
|
|
126
|
-
@connection.flush
|
|
127
|
-
# Drain the pipelined BEGIN (+telemetry) reply now that RUN+PULL are on
|
|
128
|
-
# the wire — no-op unless this is the pipelined first run. A BEGIN that
|
|
129
|
-
# failed surfaces here and is handled as a run failure below; the tx
|
|
130
|
-
# then rolls back on its way out, resetting the connection.
|
|
131
|
-
ack_begin!
|
|
132
|
-
@connection.fetch_response.assert_success!
|
|
133
|
-
rescue Exceptions::Neo4jException => e
|
|
134
|
-
@failed = true
|
|
135
|
-
# Remember the terminating error: sibling results still open must
|
|
136
|
-
# raise it (not pull) once this RUN fails — the connection is FAILED.
|
|
137
|
-
raise(@terminating_error = @connection.classify_failure(e))
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
keys = (run_response.metadata[:fields] || run_response.metadata['fields'] || []).map(&:to_sym)
|
|
141
|
-
|
|
142
|
-
@current_result = Result.new(@connection, keys, buffer: buffer, handler: handler,
|
|
143
|
-
query_text: query, parameters: parameters,
|
|
144
|
-
run_metadata: run_response.metadata, fetch_size: fetch_size,
|
|
145
|
-
qid: run_response.metadata[:qid],
|
|
146
|
-
terminated_error: method(:terminating_error))
|
|
147
|
-
@open_results << @current_result
|
|
148
|
-
@current_result
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def commit
|
|
152
|
-
# Java/JRuby-aligned messages for the already-closed states.
|
|
153
|
-
raise Exceptions::ClientException, 'Can\'t commit, transaction has been committed' if @committed
|
|
154
|
-
raise Exceptions::ClientException, 'Can\'t commit, transaction has been rolled back' if @rolled_back
|
|
155
|
-
raise Exceptions::ClientException, 'Transaction is already closed' unless @open
|
|
156
|
-
|
|
157
|
-
if terminated?
|
|
158
|
-
rollback_via_reset
|
|
159
|
-
raise Exceptions::TransactionTerminatedException,
|
|
160
|
-
"Transaction can't be committed. It has been rolled back"
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
begin
|
|
164
|
-
discard_open_results
|
|
165
|
-
rescue Exceptions::Neo4jException
|
|
166
|
-
rollback_via_reset
|
|
167
|
-
raise
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
# send/flush inside the begin block — see Transaction#run for
|
|
171
|
-
# the rationale on JRuby vs MRI socket-write timing.
|
|
172
|
-
response =
|
|
173
|
-
begin
|
|
174
|
-
@connection.send_message(Bolt::Message.commit)
|
|
175
|
-
@connection.flush
|
|
176
|
-
@connection.fetch_response.assert_success!
|
|
177
|
-
rescue Exceptions::Neo4jException => e
|
|
178
|
-
@failed = true
|
|
179
|
-
# Classify first so a security failure flags the connection
|
|
180
|
-
# for discard before rollback_via_reset releases it.
|
|
181
|
-
classified = @connection.classify_failure(e)
|
|
182
|
-
rollback_via_reset
|
|
183
|
-
raise classified
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
@committed = true
|
|
187
|
-
@open = false
|
|
188
|
-
|
|
189
|
-
bookmarks = response.metadata[:bookmark]
|
|
190
|
-
@session.update_bookmarks(bookmarks) if bookmarks
|
|
191
|
-
release_connection
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def rollback
|
|
195
|
-
raise Exceptions::ClientException, 'Transaction is already closed' unless @open
|
|
196
|
-
|
|
197
|
-
# A pipelined executeQuery tx whose query never ran (e.g. local validation
|
|
198
|
-
# failed before RUN/PULL were sent) left BEGIN's reply unread — and a
|
|
199
|
-
# pipelining server may withhold it until RUN/PULL arrive, which now never
|
|
200
|
-
# will. RESET rolls the tx back and drains any pending reply without a
|
|
201
|
-
# blocking read that could deadlock; there are no open results to discard.
|
|
202
|
-
return rollback_via_reset unless @begin_acked
|
|
203
|
-
|
|
204
|
-
# A terminated tx left the connection FAILED: don't drain open results
|
|
205
|
-
# (that would send PULL/DISCARD the server rejects) — just RESET.
|
|
206
|
-
if terminated?
|
|
207
|
-
rollback_via_reset
|
|
208
|
-
return
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
begin
|
|
212
|
-
discard_open_results
|
|
213
|
-
rescue Exceptions::Neo4jException
|
|
214
|
-
# Failures surfaced while draining a pending result are expected
|
|
215
|
-
# during rollback — the tx is being discarded anyway. @failed is
|
|
216
|
-
# set; RESET path below will clean up the connection.
|
|
217
|
-
end
|
|
218
|
-
|
|
219
|
-
if @failed
|
|
220
|
-
rollback_via_reset
|
|
221
|
-
return
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
begin
|
|
225
|
-
@connection.send_message(Bolt::Message.rollback)
|
|
226
|
-
@connection.flush
|
|
227
|
-
@connection.fetch_response.assert_success!
|
|
228
|
-
rescue Exceptions::ServiceUnavailableException, Exceptions::SessionExpiredException
|
|
229
|
-
# Rolling back on a broken/dead connection is a no-op — the
|
|
230
|
-
# server discards the tx when the link dies. Swallow these so
|
|
231
|
-
# session.close's rollback path stays clean.
|
|
232
|
-
rescue Exceptions::Neo4jException => e
|
|
233
|
-
# A server FAILURE on ROLLBACK (e.g. DatabaseUnavailable) is a
|
|
234
|
-
# real error: the connection is now in FAILED state. RESET it
|
|
235
|
-
# back to READY, then surface the failure through the routing
|
|
236
|
-
# classifier — same as commit/run — so routing side effects
|
|
237
|
-
# (e.g. deactivate on DatabaseUnavailable) fire and the surfaced
|
|
238
|
-
# type is consistent. No-op for direct connections.
|
|
239
|
-
@connection.reset!
|
|
240
|
-
raise @connection.classify_failure(e)
|
|
241
|
-
ensure
|
|
242
|
-
@rolled_back = true
|
|
243
|
-
@open = false
|
|
244
|
-
release_connection
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def close
|
|
249
|
-
rollback if @open && !@committed
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def open?
|
|
253
|
-
@open
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
def failed?
|
|
257
|
-
@failed
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
private
|
|
261
|
-
|
|
262
|
-
# Read the deferred BEGIN acknowledgement (and the telemetry SUCCESS
|
|
263
|
-
# pipelined ahead of it). Idempotent: called once — eagerly in #initialize
|
|
264
|
-
# for a normal tx, or after the first RUN+PULL flush for a pipelined
|
|
265
|
-
# executeQuery (and defensively before ROLLBACK if that query never ran).
|
|
266
|
-
def ack_begin!
|
|
267
|
-
return if @begin_acked
|
|
268
|
-
|
|
269
|
-
@begin_acked = true
|
|
270
|
-
|
|
271
|
-
if @telemetry_sent
|
|
272
|
-
@connection.fetch_response.assert_success!
|
|
273
|
-
# The server acknowledged telemetry; a managed-tx retry won't re-send it.
|
|
274
|
-
@telemetry_ack&.call
|
|
275
|
-
end
|
|
276
|
-
begin_response = @connection.fetch_response.assert_success!
|
|
277
|
-
# A home-db BEGIN that sent db=nil comes back with the resolved name.
|
|
278
|
-
@on_begin&.call(begin_response)
|
|
279
|
-
begin_response
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
# The transaction is terminated once a server failure has hit it — either
|
|
283
|
-
# a tx method caught it (@failed) or any open result failed during the
|
|
284
|
-
# user's own iteration (its failure hasn't passed through a tx method).
|
|
285
|
-
def terminated? = @failed || @open_results.any?(&:failed?)
|
|
286
|
-
|
|
287
|
-
# The error that terminated this tx, or nil. A RUN/commit failure records
|
|
288
|
-
# it directly; a result that failed mid-iteration (the user's own PULL)
|
|
289
|
-
# carries it on the result. Passed to each result as its terminated_error
|
|
290
|
-
# so a sibling raises it instead of pulling on a FAILED connection.
|
|
291
|
-
def terminating_error = @terminating_error || @open_results.find(&:failed?)&.failure
|
|
292
|
-
|
|
293
|
-
# See Session#effective_fetch_size. Transactions inherit the session
|
|
294
|
-
# options at open, so the same default rules apply.
|
|
295
|
-
def effective_fetch_size
|
|
296
|
-
size = @options[:fetch_size]
|
|
297
|
-
size.nil? ? 1000 : size
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# At tx end (commit/rollback) every still-open result goes out of scope,
|
|
301
|
-
# so discard each — DISCARD abandons remaining records (essential when a
|
|
302
|
-
# result is unbounded) rather than streaming them into memory, and leaves
|
|
303
|
-
# each raising ResultConsumedException on later access. Demoted results
|
|
304
|
-
# DISCARD by their qid; the current one omits it (targets the last query).
|
|
305
|
-
def discard_open_results
|
|
306
|
-
@open_results.each do |result|
|
|
307
|
-
begin
|
|
308
|
-
result.consume
|
|
309
|
-
rescue Exceptions::Neo4jException => e
|
|
310
|
-
@failed = true
|
|
311
|
-
# A wire error during PULL streaming (e.g. a reader connection
|
|
312
|
-
# interrupted mid-stream) raises ServiceUnavailable straight from
|
|
313
|
-
# fetch_response, not via Result#on_failure — so it never saw the
|
|
314
|
-
# routing classifier. Run it through here so a routed connection
|
|
315
|
-
# failure surfaces as SessionExpired (idempotent if already classified).
|
|
316
|
-
raise @connection.classify_failure(e)
|
|
317
|
-
end
|
|
318
|
-
|
|
319
|
-
# consume is a no-op when the result was already drained by the user;
|
|
320
|
-
# surface any stored failure so callers can react.
|
|
321
|
-
@failed = true if result.failed?
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
# Recover a failed transaction by asking the server to RESET the
|
|
326
|
-
# connection. RESET transitions the server from FAILED back to READY
|
|
327
|
-
# and implicitly rolls back the open transaction.
|
|
328
|
-
def rollback_via_reset
|
|
329
|
-
# Skip RESET on a connection being discarded (auth failure: the
|
|
330
|
-
# server closes it, RESET would just error); release then honors
|
|
331
|
-
# the discard flag so it isn't pooled.
|
|
332
|
-
@connection.reset! unless @connection.auth_failed
|
|
333
|
-
@rolled_back = true
|
|
334
|
-
@open = false
|
|
335
|
-
release_connection
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def release_connection
|
|
339
|
-
@on_release&.call
|
|
340
|
-
@on_release = nil # idempotent
|
|
21
|
+
@transaction.run(query, **parameters)
|
|
341
22
|
end
|
|
342
23
|
end
|
|
343
24
|
end
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Neo4j
|
|
4
|
+
module Driver
|
|
5
|
+
# An explicit (unmanaged) transaction: the full lifecycle — run, commit,
|
|
6
|
+
# rollback, close. This is what session.begin_transaction yields/returns.
|
|
7
|
+
# Managed functions (execute_read/execute_write) instead yield a
|
|
8
|
+
# Transaction, a thin context that exposes only #run and delegates to an
|
|
9
|
+
# UnmanagedTransaction the driver owns.
|
|
10
|
+
class UnmanagedTransaction
|
|
11
|
+
attr_reader :connection
|
|
12
|
+
|
|
13
|
+
def initialize(connection, session, bookmarks = [], options = {}, telemetry_api: nil, telemetry_ack: nil,
|
|
14
|
+
pipelined: false, on_begin: nil, on_release: nil)
|
|
15
|
+
@connection = connection
|
|
16
|
+
@session = session
|
|
17
|
+
@options = options
|
|
18
|
+
# executeQuery pipelines BEGIN + RUN + PULL (Optimization:ExecuteQueryPipelining):
|
|
19
|
+
# BEGIN's reply is read only after the first RUN+PULL are flushed, not eagerly.
|
|
20
|
+
@pipelined = pipelined
|
|
21
|
+
@on_begin = on_begin # called with the BEGIN reply (home-db cache update)
|
|
22
|
+
@on_release = on_release # called once when the connection is no longer needed
|
|
23
|
+
@open = true
|
|
24
|
+
@committed = false
|
|
25
|
+
@rolled_back = false
|
|
26
|
+
@failed = false
|
|
27
|
+
@terminating_error = nil # the classified error that terminated this tx (a RUN/commit failure)
|
|
28
|
+
@current_result = nil
|
|
29
|
+
# Every result opened in this tx, in order. Bolt lets multiple stay open
|
|
30
|
+
# and streaming concurrently (qid multiplexing); a new RUN no longer
|
|
31
|
+
# force-buffers the previous one, so we track them all here to discard
|
|
32
|
+
# any still-open at commit/rollback and to spot a mid-stream failure.
|
|
33
|
+
@open_results = []
|
|
34
|
+
|
|
35
|
+
# Begin the transaction
|
|
36
|
+
# Drop blank values so the serialised BEGIN map matches what
|
|
37
|
+
# testkit's stub scripts expect (e.g. `BEGIN {"db": "adb"}`,
|
|
38
|
+
# not `BEGIN {"db": "adb", "tx_metadata": {}}`).
|
|
39
|
+
begin_extra = {
|
|
40
|
+
bookmarks: bookmarks,
|
|
41
|
+
db: options[:database],
|
|
42
|
+
mode: options[:access_mode],
|
|
43
|
+
tx_timeout: options[:timeout],
|
|
44
|
+
tx_metadata: options[:metadata],
|
|
45
|
+
imp_user: options[:impersonated_user]
|
|
46
|
+
}
|
|
47
|
+
begin_extra.reject!(&Internal::Extras::BLANK)
|
|
48
|
+
|
|
49
|
+
# TELEMETRY (api = 0 managed / 1 explicit / 3 executeQuery) pipelined
|
|
50
|
+
# ahead of BEGIN when the server opted in and the driver didn't disable
|
|
51
|
+
# it; its SUCCESS is read first (see #ack_begin!).
|
|
52
|
+
@telemetry_sent = @connection.telemetry(telemetry_api, disabled: options[:telemetry_disabled])
|
|
53
|
+
@telemetry_ack = telemetry_ack
|
|
54
|
+
# Session-level NotificationsConfig rides on BEGIN (5.2+); the tx's own
|
|
55
|
+
# RUNs carry none. nil / pre-5.2 => no notification keys on the wire.
|
|
56
|
+
@connection.send_message(@connection.protocol.build_begin(begin_extra,
|
|
57
|
+
notification_config: options[:notification_config]))
|
|
58
|
+
@connection.flush
|
|
59
|
+
|
|
60
|
+
# Non-pipelined: read BEGIN's reply now, a plain round-trip (unchanged).
|
|
61
|
+
# Pipelined (executeQuery): defer it so the first #run can flush RUN+PULL
|
|
62
|
+
# before we block — a pipelining server withholds BEGIN's SUCCESS until it
|
|
63
|
+
# has all three. #ack_begin! drains it after that flush.
|
|
64
|
+
@begin_acked = false
|
|
65
|
+
ack_begin! unless @pipelined
|
|
66
|
+
rescue Exceptions::Neo4jException => e
|
|
67
|
+
# Classify first so the auth-token manager is notified and the
|
|
68
|
+
# connection is flagged for discard on an auth failure (the server
|
|
69
|
+
# closes it). BEGIN failed → server is in FAILED state; RESET to
|
|
70
|
+
# make it reusable, unless it's being discarded (a security
|
|
71
|
+
# failure: the server closes it and RESET would just error).
|
|
72
|
+
classified = @connection.classify_failure(e)
|
|
73
|
+
@connection.reset! unless @connection.auth_failed
|
|
74
|
+
@open = false
|
|
75
|
+
release_connection
|
|
76
|
+
raise classified
|
|
77
|
+
rescue StandardError
|
|
78
|
+
# Transport-level failure (IO/socket). RESET will likely fail
|
|
79
|
+
# too on a dead connection, but release the lease so it doesn't
|
|
80
|
+
# leak; pool reuse will surface the breakage to the next caller.
|
|
81
|
+
@open = false
|
|
82
|
+
release_connection
|
|
83
|
+
raise
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def run(query, **parameters)
|
|
87
|
+
Internal::Validator.require_query_text!(query)
|
|
88
|
+
# A server failure terminated this transaction; further work is
|
|
89
|
+
# rejected locally (no wire traffic) as a TransactionTerminatedException
|
|
90
|
+
# — a ClientException subclass, matching the Java driver. Covers both a
|
|
91
|
+
# failed RUN (sets @failed) and a result that failed mid-stream during
|
|
92
|
+
# the user's own iteration (@current_result.failed?). Message wording
|
|
93
|
+
# stays "rolled back" to match the Java flavor (a shared integration spec
|
|
94
|
+
# asserts it on both impls); only the exception class becomes specific.
|
|
95
|
+
if terminated?
|
|
96
|
+
raise Exceptions::TransactionTerminatedException,
|
|
97
|
+
'Cannot run more queries in this transaction, it has been rolled back'
|
|
98
|
+
end
|
|
99
|
+
unless @open
|
|
100
|
+
# Mirror the Java/JRuby messages so the closed-state reason
|
|
101
|
+
# (committed vs rolled back) is reported the same on both impls.
|
|
102
|
+
raise Exceptions::ClientException,
|
|
103
|
+
"Cannot run more queries in this transaction, it has been #{@committed ? 'committed' : 'rolled back'}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A new query becomes current; the previous result must now name its qid
|
|
107
|
+
# explicitly on further PULL/DISCARD (the server defaults them to the last
|
|
108
|
+
# opened query). We keep it open and streaming rather than buffering it —
|
|
109
|
+
# that's the qid multiplexing the nested-result tests exercise.
|
|
110
|
+
@current_result&.demote!
|
|
111
|
+
|
|
112
|
+
fetch_size = effective_fetch_size
|
|
113
|
+
|
|
114
|
+
# send/flush are inside the begin block so a transport-level
|
|
115
|
+
# failure surfacing on fetch_response still sets @failed and
|
|
116
|
+
# goes through classify_failure. Connection#send_message and
|
|
117
|
+
# #flush both defer peer-closed errors so a buffered server
|
|
118
|
+
# FAILURE is read before fetch_response can raise its own
|
|
119
|
+
# EOF-driven ServiceUnavailableException — JRuby surfaces
|
|
120
|
+
# EPIPE eagerly, MRI tends to defer it naturally. Without
|
|
121
|
+
# this rescue placement, session.close's subsequent rollback
|
|
122
|
+
# path takes the ROLLBACK-message branch (rather than
|
|
123
|
+
# rollback_via_reset) and re-fails on the dead connection.
|
|
124
|
+
buffer = Bolt::RecordBuffer.new(fetch_size: fetch_size)
|
|
125
|
+
handler = Bolt::StreamHandler.new(buffer)
|
|
126
|
+
run_response =
|
|
127
|
+
begin
|
|
128
|
+
@connection.send_message(@connection.protocol.build_run(query, parameters, {}))
|
|
129
|
+
@connection.send_message(@connection.protocol.build_pull(n: fetch_size), handler)
|
|
130
|
+
@connection.flush
|
|
131
|
+
# Drain the pipelined BEGIN (+telemetry) reply now that RUN+PULL are on
|
|
132
|
+
# the wire — no-op unless this is the pipelined first run. A BEGIN that
|
|
133
|
+
# failed surfaces here and is handled as a run failure below; the tx
|
|
134
|
+
# then rolls back on its way out, resetting the connection.
|
|
135
|
+
ack_begin!
|
|
136
|
+
@connection.fetch_response.assert_success!
|
|
137
|
+
rescue Exceptions::Neo4jException => e
|
|
138
|
+
@failed = true
|
|
139
|
+
# Remember the terminating error: sibling results still open must
|
|
140
|
+
# raise it (not pull) once this RUN fails — the connection is FAILED.
|
|
141
|
+
raise(@terminating_error = @connection.classify_failure(e))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
keys = (run_response.metadata[:fields] || run_response.metadata['fields'] || []).map(&:to_sym)
|
|
145
|
+
|
|
146
|
+
@current_result = Result.new(@connection, keys, buffer: buffer, handler: handler,
|
|
147
|
+
query_text: query, parameters: parameters,
|
|
148
|
+
run_metadata: run_response.metadata, fetch_size: fetch_size,
|
|
149
|
+
qid: run_response.metadata[:qid],
|
|
150
|
+
terminated_error: method(:terminating_error))
|
|
151
|
+
@open_results << @current_result
|
|
152
|
+
@current_result
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def commit
|
|
156
|
+
# Java/JRuby-aligned messages for the already-closed states.
|
|
157
|
+
raise Exceptions::ClientException, 'Can\'t commit, transaction has been committed' if @committed
|
|
158
|
+
raise Exceptions::ClientException, 'Can\'t commit, transaction has been rolled back' if @rolled_back
|
|
159
|
+
raise Exceptions::ClientException, 'Transaction is already closed' unless @open
|
|
160
|
+
|
|
161
|
+
if terminated?
|
|
162
|
+
rollback_via_reset
|
|
163
|
+
raise Exceptions::TransactionTerminatedException,
|
|
164
|
+
"Transaction can't be committed. It has been rolled back"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
discard_open_results
|
|
169
|
+
rescue Exceptions::Neo4jException
|
|
170
|
+
rollback_via_reset
|
|
171
|
+
raise
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# send/flush inside the begin block — see Transaction#run for
|
|
175
|
+
# the rationale on JRuby vs MRI socket-write timing.
|
|
176
|
+
response =
|
|
177
|
+
begin
|
|
178
|
+
@connection.send_message(Bolt::Message.commit)
|
|
179
|
+
@connection.flush
|
|
180
|
+
@connection.fetch_response.assert_success!
|
|
181
|
+
rescue Exceptions::Neo4jException => e
|
|
182
|
+
@failed = true
|
|
183
|
+
# Classify first so a security failure flags the connection
|
|
184
|
+
# for discard before rollback_via_reset releases it.
|
|
185
|
+
classified = @connection.classify_failure(e)
|
|
186
|
+
rollback_via_reset
|
|
187
|
+
raise classified
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
@committed = true
|
|
191
|
+
@open = false
|
|
192
|
+
|
|
193
|
+
bookmarks = response.metadata[:bookmark]
|
|
194
|
+
@session.update_bookmarks(bookmarks) if bookmarks
|
|
195
|
+
release_connection
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def rollback
|
|
199
|
+
raise Exceptions::ClientException, 'Transaction is already closed' unless @open
|
|
200
|
+
|
|
201
|
+
# A pipelined executeQuery tx whose query never ran (e.g. local validation
|
|
202
|
+
# failed before RUN/PULL were sent) left BEGIN's reply unread — and a
|
|
203
|
+
# pipelining server may withhold it until RUN/PULL arrive, which now never
|
|
204
|
+
# will. RESET rolls the tx back and drains any pending reply without a
|
|
205
|
+
# blocking read that could deadlock; there are no open results to discard.
|
|
206
|
+
return rollback_via_reset unless @begin_acked
|
|
207
|
+
|
|
208
|
+
# A terminated tx left the connection FAILED: don't drain open results
|
|
209
|
+
# (that would send PULL/DISCARD the server rejects) — just RESET.
|
|
210
|
+
if terminated?
|
|
211
|
+
rollback_via_reset
|
|
212
|
+
return
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
begin
|
|
216
|
+
discard_open_results
|
|
217
|
+
rescue Exceptions::Neo4jException
|
|
218
|
+
# Failures surfaced while draining a pending result are expected
|
|
219
|
+
# during rollback — the tx is being discarded anyway. @failed is
|
|
220
|
+
# set; RESET path below will clean up the connection.
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
if @failed
|
|
224
|
+
rollback_via_reset
|
|
225
|
+
return
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
begin
|
|
229
|
+
@connection.send_message(Bolt::Message.rollback)
|
|
230
|
+
@connection.flush
|
|
231
|
+
@connection.fetch_response.assert_success!
|
|
232
|
+
rescue Exceptions::ServiceUnavailableException, Exceptions::SessionExpiredException
|
|
233
|
+
# Rolling back on a broken/dead connection is a no-op — the
|
|
234
|
+
# server discards the tx when the link dies. Swallow these so
|
|
235
|
+
# session.close's rollback path stays clean.
|
|
236
|
+
rescue Exceptions::Neo4jException => e
|
|
237
|
+
# A server FAILURE on ROLLBACK (e.g. DatabaseUnavailable) is a
|
|
238
|
+
# real error: the connection is now in FAILED state. RESET it
|
|
239
|
+
# back to READY, then surface the failure through the routing
|
|
240
|
+
# classifier — same as commit/run — so routing side effects
|
|
241
|
+
# (e.g. deactivate on DatabaseUnavailable) fire and the surfaced
|
|
242
|
+
# type is consistent. No-op for direct connections.
|
|
243
|
+
@connection.reset!
|
|
244
|
+
raise @connection.classify_failure(e)
|
|
245
|
+
ensure
|
|
246
|
+
@rolled_back = true
|
|
247
|
+
@open = false
|
|
248
|
+
release_connection
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def close
|
|
253
|
+
rollback if @open && !@committed
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def open?
|
|
257
|
+
@open
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def failed?
|
|
261
|
+
@failed
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
private
|
|
265
|
+
|
|
266
|
+
# Read the deferred BEGIN acknowledgement (and the telemetry SUCCESS
|
|
267
|
+
# pipelined ahead of it). Idempotent: called once — eagerly in #initialize
|
|
268
|
+
# for a normal tx, or after the first RUN+PULL flush for a pipelined
|
|
269
|
+
# executeQuery (and defensively before ROLLBACK if that query never ran).
|
|
270
|
+
def ack_begin!
|
|
271
|
+
return if @begin_acked
|
|
272
|
+
|
|
273
|
+
@begin_acked = true
|
|
274
|
+
|
|
275
|
+
if @telemetry_sent
|
|
276
|
+
@connection.fetch_response.assert_success!
|
|
277
|
+
# The server acknowledged telemetry; a managed-tx retry won't re-send it.
|
|
278
|
+
@telemetry_ack&.call
|
|
279
|
+
end
|
|
280
|
+
begin_response = @connection.fetch_response.assert_success!
|
|
281
|
+
# A home-db BEGIN that sent db=nil comes back with the resolved name.
|
|
282
|
+
@on_begin&.call(begin_response)
|
|
283
|
+
begin_response
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# The transaction is terminated once a server failure has hit it — either
|
|
287
|
+
# a tx method caught it (@failed) or any open result failed during the
|
|
288
|
+
# user's own iteration (its failure hasn't passed through a tx method).
|
|
289
|
+
def terminated? = @failed || @open_results.any?(&:failed?)
|
|
290
|
+
|
|
291
|
+
# The error that terminated this tx, or nil. A RUN/commit failure records
|
|
292
|
+
# it directly; a result that failed mid-iteration (the user's own PULL)
|
|
293
|
+
# carries it on the result. Passed to each result as its terminated_error
|
|
294
|
+
# so a sibling raises it instead of pulling on a FAILED connection.
|
|
295
|
+
def terminating_error = @terminating_error || @open_results.find(&:failed?)&.failure
|
|
296
|
+
|
|
297
|
+
# See Session#effective_fetch_size. Transactions inherit the session
|
|
298
|
+
# options at open, so the same default rules apply.
|
|
299
|
+
def effective_fetch_size
|
|
300
|
+
size = @options[:fetch_size]
|
|
301
|
+
size.nil? ? 1000 : size
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# At tx end (commit/rollback) every still-open result goes out of scope,
|
|
305
|
+
# so discard each — DISCARD abandons remaining records (essential when a
|
|
306
|
+
# result is unbounded) rather than streaming them into memory, and leaves
|
|
307
|
+
# each raising ResultConsumedException on later access. Demoted results
|
|
308
|
+
# DISCARD by their qid; the current one omits it (targets the last query).
|
|
309
|
+
def discard_open_results
|
|
310
|
+
@open_results.each do |result|
|
|
311
|
+
begin
|
|
312
|
+
result.consume
|
|
313
|
+
rescue Exceptions::Neo4jException => e
|
|
314
|
+
@failed = true
|
|
315
|
+
# A wire error during PULL streaming (e.g. a reader connection
|
|
316
|
+
# interrupted mid-stream) raises ServiceUnavailable straight from
|
|
317
|
+
# fetch_response, not via Result#on_failure — so it never saw the
|
|
318
|
+
# routing classifier. Run it through here so a routed connection
|
|
319
|
+
# failure surfaces as SessionExpired (idempotent if already classified).
|
|
320
|
+
raise @connection.classify_failure(e)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# consume is a no-op when the result was already drained by the user;
|
|
324
|
+
# surface any stored failure so callers can react.
|
|
325
|
+
@failed = true if result.failed?
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Recover a failed transaction by asking the server to RESET the
|
|
330
|
+
# connection. RESET transitions the server from FAILED back to READY
|
|
331
|
+
# and implicitly rolls back the open transaction.
|
|
332
|
+
def rollback_via_reset
|
|
333
|
+
# Skip RESET on a connection being discarded (auth failure: the
|
|
334
|
+
# server closes it, RESET would just error); release then honors
|
|
335
|
+
# the discard flag so it isn't pooled.
|
|
336
|
+
@connection.reset! unless @connection.auth_failed
|
|
337
|
+
@rolled_back = true
|
|
338
|
+
@open = false
|
|
339
|
+
release_connection
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def release_connection
|
|
343
|
+
@on_release&.call
|
|
344
|
+
@on_release = nil # idempotent
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
data/lib/neo4j/driver/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: neo4j-ruby-driver
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.2.1
|
|
4
|
+
version: 6.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Neo4j Driver Team
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- lib/neo4j/driver/types/unresolvable_zoned_date_time.rb
|
|
189
189
|
- lib/neo4j/driver/types/unsupported_type.rb
|
|
190
190
|
- lib/neo4j/driver/types/uuid.rb
|
|
191
|
+
- lib/neo4j/driver/unmanaged_transaction.rb
|
|
191
192
|
- lib/neo4j/driver/version.rb
|
|
192
193
|
- lib/neo4j_ruby_driver.rb
|
|
193
194
|
homepage: https://github.com/neo4jrb/neo4j-ruby-driver
|