activecypher 0.15.4 → 0.15.5
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd37ad6691e4ec34e421cde2caa055e8379d9d15e0b6df558b60fdcc76f6bd6f
|
|
4
|
+
data.tar.gz: 7284b02415749cab48ec434dd689d7a4c9bec23a3bdf62f6636e32f0bb99501b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: befe8d23ced99f81ab0d01195000aa22e1c9e35f10a0268de8a3627010721720b223d3ad02b1116d9487196cdec2408a7e4079325284630afec1fb684f42a295
|
|
7
|
+
data.tar.gz: 53e90b852ecd80cf949c6df0d0c5ed5589e52ba043326f84e2f52206ee7f22de637522eb5770d1168396a34d3d9679db3abea3462bf2395ac185f2b81e6c53b2
|
|
@@ -77,7 +77,7 @@ module ActiveCypher
|
|
|
77
77
|
@state = :failed
|
|
78
78
|
code = msg.metadata['code']
|
|
79
79
|
message = msg.metadata['message']
|
|
80
|
-
raise
|
|
80
|
+
raise query_error_for(code), "Query execution failed: #{code} - #{message}"
|
|
81
81
|
else
|
|
82
82
|
raise ProtocolError, "Unexpected message type: #{msg.class}"
|
|
83
83
|
end
|
|
@@ -90,7 +90,7 @@ module ActiveCypher
|
|
|
90
90
|
@state = :failed
|
|
91
91
|
code = response.metadata['code']
|
|
92
92
|
message = response.metadata['message']
|
|
93
|
-
raise
|
|
93
|
+
raise query_error_for(code), "Query execution failed: #{code} - #{message}"
|
|
94
94
|
else
|
|
95
95
|
raise ProtocolError, "Unexpected response to RUN: #{response.class}"
|
|
96
96
|
end
|
|
@@ -204,6 +204,18 @@ module ActiveCypher
|
|
|
204
204
|
def failed?
|
|
205
205
|
@state == :failed
|
|
206
206
|
end
|
|
207
|
+
|
|
208
|
+
private
|
|
209
|
+
|
|
210
|
+
# Server error code -> exception class. Substring match covers both
|
|
211
|
+
# Neo4j and Memgraph spellings of the same codes.
|
|
212
|
+
def query_error_for(code)
|
|
213
|
+
c = code.to_s
|
|
214
|
+
return ConstraintError if c.include?('Constraint')
|
|
215
|
+
return TransientError if c.include?('TransientError')
|
|
216
|
+
|
|
217
|
+
QueryError
|
|
218
|
+
end
|
|
207
219
|
end
|
|
208
220
|
end
|
|
209
221
|
end
|
|
@@ -112,6 +112,8 @@ module ActiveCypher
|
|
|
112
112
|
end
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
TRANSIENT_RETRIES = 3
|
|
116
|
+
|
|
115
117
|
# Override run to execute queries using auto-commit mode.
|
|
116
118
|
# Memgraph auto-commits each query, so we send RUN + PULL directly
|
|
117
119
|
# without BEGIN/COMMIT wrapper. This avoids transaction state issues.
|
|
@@ -120,7 +122,26 @@ module ActiveCypher
|
|
|
120
122
|
logger.debug { "[#{context}] #{cypher} #{params.inspect}" }
|
|
121
123
|
|
|
122
124
|
instrument_query(cypher, params, context: context, metadata: { db: db, access_mode: access_mode }) do
|
|
123
|
-
run_auto_commit(cypher, prepare_params(params))
|
|
125
|
+
with_transient_retry(context) { run_auto_commit(cypher, prepare_params(params)) }
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Retry write/write conflicts the server flagged as transient. Safe only
|
|
130
|
+
# because this path is auto-commit (the whole query rolled back); do NOT
|
|
131
|
+
# lift into the explicit-transaction path, where earlier statements may
|
|
132
|
+
# already have applied.
|
|
133
|
+
def with_transient_retry(context)
|
|
134
|
+
attempts = 0
|
|
135
|
+
begin
|
|
136
|
+
yield
|
|
137
|
+
rescue ActiveCypher::TransientError => e
|
|
138
|
+
attempts += 1
|
|
139
|
+
raise if attempts > TRANSIENT_RETRIES
|
|
140
|
+
|
|
141
|
+
# Full jitter, so conflicting writers don't back off in step.
|
|
142
|
+
sleep(rand * 0.05 * (2**(attempts - 1)))
|
|
143
|
+
logger.debug { "[#{context}] transient conflict, retry #{attempts}/#{TRANSIENT_RETRIES}: #{e.message}" }
|
|
144
|
+
retry
|
|
124
145
|
end
|
|
125
146
|
end
|
|
126
147
|
|
|
@@ -25,27 +25,40 @@ module ActiveCypher
|
|
|
25
25
|
@spec = resolved_config.merge(@spec.except(:url))
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
# One connection per thread: Bolt is a stateful, ordered protocol, so
|
|
29
|
+
# sharing a socket across threads corrupts the stream. @connections
|
|
30
|
+
# exists only so disconnect can close them all; the thread-local is
|
|
31
|
+
# what the hot path reads.
|
|
32
|
+
@connections = {}
|
|
33
|
+
@creation_mutex = Mutex.new
|
|
30
34
|
end
|
|
31
35
|
|
|
32
|
-
# Returns a live adapter
|
|
36
|
+
# Returns a live adapter belonging to the calling thread.
|
|
33
37
|
def connection
|
|
34
|
-
|
|
35
|
-
conn = @conn_ref
|
|
38
|
+
conn = Thread.current[thread_key]
|
|
36
39
|
return conn if conn&.active?
|
|
37
40
|
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
# Built outside the mutex: connecting does network IO.
|
|
42
|
+
new_conn = build_connection
|
|
43
|
+
Thread.current[thread_key] = new_conn
|
|
44
|
+
|
|
45
|
+
# Reap dead threads' connections, or each short-lived thread leaks a
|
|
46
|
+
# socket the server counts against max_connections.
|
|
47
|
+
orphaned = @creation_mutex.synchronize do
|
|
48
|
+
dead = @connections.reject { |t, _| t.alive? }
|
|
49
|
+
dead.each_key { |t| @connections.delete(t) }
|
|
50
|
+
@connections[Thread.current] = new_conn
|
|
51
|
+
dead.values
|
|
52
|
+
end
|
|
43
53
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
# Closed outside the mutex: disconnecting does IO too.
|
|
55
|
+
orphaned.each do |conn|
|
|
56
|
+
conn.disconnect
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
puts "Warning: Error disconnecting orphaned connection: #{e.message}" if ENV['DEBUG']
|
|
48
59
|
end
|
|
60
|
+
|
|
61
|
+
new_conn
|
|
49
62
|
end
|
|
50
63
|
alias checkout connection
|
|
51
64
|
|
|
@@ -54,23 +67,31 @@ module ActiveCypher
|
|
|
54
67
|
@retry_count >= @spec[:max_retries]
|
|
55
68
|
end
|
|
56
69
|
|
|
57
|
-
# Explicitly close
|
|
70
|
+
# Explicitly close every connection this pool handed out.
|
|
58
71
|
def disconnect
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
conns = @creation_mutex.synchronize do
|
|
73
|
+
taken = @connections.values
|
|
74
|
+
@connections.clear
|
|
75
|
+
taken
|
|
76
|
+
end
|
|
61
77
|
|
|
62
|
-
|
|
78
|
+
conns.each do |conn|
|
|
63
79
|
conn.disconnect
|
|
64
80
|
rescue StandardError => e
|
|
65
81
|
# Log but don't raise to ensure cleanup continues
|
|
66
82
|
puts "Warning: Error disconnecting: #{e.message}" if ENV['DEBUG']
|
|
67
|
-
ensure
|
|
68
|
-
@conn_ref = nil
|
|
69
83
|
end
|
|
84
|
+
|
|
85
|
+
Thread.current[thread_key] = nil
|
|
70
86
|
end
|
|
71
87
|
|
|
72
88
|
private
|
|
73
89
|
|
|
90
|
+
# Namespaced per pool instance so multiple databases don't share.
|
|
91
|
+
def thread_key
|
|
92
|
+
@thread_key ||= :"active_cypher_connection_#{object_id}"
|
|
93
|
+
end
|
|
94
|
+
|
|
74
95
|
def build_connection
|
|
75
96
|
adapter_name = @spec[:adapter]
|
|
76
97
|
raise ArgumentError, 'Missing adapter name in connection specification' unless adapter_name
|
data/lib/activecypher.rb
CHANGED
|
@@ -52,6 +52,14 @@ module ActiveCypher
|
|
|
52
52
|
# Could be you. Could be Cypher. Could be fate.
|
|
53
53
|
class QueryError < Error; end
|
|
54
54
|
|
|
55
|
+
# A uniqueness or existence constraint said no.
|
|
56
|
+
# Rescue this to ignore duplicates without swallowing every QueryError.
|
|
57
|
+
class ConstraintError < QueryError; end
|
|
58
|
+
|
|
59
|
+
# Two writers touched the same node. Nothing is wrong with your query.
|
|
60
|
+
# The server is politely asking you to try again.
|
|
61
|
+
class TransientError < QueryError; end
|
|
62
|
+
|
|
55
63
|
# Your Cypher syntax is... interpretive.
|
|
56
64
|
# Unfortunately, the parser isn’t in the mood for interpretive dance.
|
|
57
65
|
class CypherSyntaxError < QueryError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: activecypher
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.15.
|
|
4
|
+
version: 0.15.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -293,7 +293,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
293
293
|
- !ruby/object:Gem::Version
|
|
294
294
|
version: '0'
|
|
295
295
|
requirements: []
|
|
296
|
-
rubygems_version: 4.0.
|
|
296
|
+
rubygems_version: 4.0.10
|
|
297
297
|
specification_version: 4
|
|
298
298
|
summary: OpenCypher Adapter ala ActiveRecord
|
|
299
299
|
test_files: []
|