Skip to content

Commit a24194f

Browse files
committed
invalid transactions for indicating failed transactions on the server side
1 parent aa69345 commit a24194f

File tree

6 files changed

+277
-122
lines changed

6 files changed

+277
-122
lines changed

lib/neo4j-server/cypher_response.rb

+67-42
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
module Neo4j
22
module Server
33
class CypherResponse
4-
attr_reader :data, :columns, :error_msg, :error_status, :error_code, :response
4+
attr_reader :data, :columns, :response
55

66
class ResponseError < StandardError
77
attr_reader :status, :code
@@ -13,7 +13,6 @@ def initialize(msg, status, code)
1313
end
1414
end
1515

16-
1716
class HashEnumeration
1817
include Enumerable
1918
extend Forwardable
@@ -89,12 +88,13 @@ def hash_value_as_object(value, session)
8988
attr_reader :struct
9089

9190
def initialize(response, uncommited = false)
92-
@response = response
91+
@response = response
9392
@uncommited = uncommited
93+
set_data_from_request if response
9494
end
9595

96-
def entity_data(id = nil)
97-
if @uncommited
96+
def entity_data(id=nil)
97+
if uncommited?
9898
data = @data.first['row'].first
9999
data.is_a?(Hash) ? {'data' => data, 'id' => id} : data
100100
else
@@ -104,7 +104,7 @@ def entity_data(id = nil)
104104
end
105105

106106
def first_data(id = nil)
107-
if @uncommited
107+
if uncommited?
108108
data = @data.first['row'].first
109109
# data.is_a?(Hash) ? {'data' => data, 'id' => id} : data
110110
else
@@ -121,8 +121,43 @@ def add_transaction_entity_id
121121
mapped_rest_data.merge!('id' => mapped_rest_data['self'].split('/').last.to_i)
122122
end
123123

124+
def errors
125+
transaction_response? ? transaction_errors : non_transaction_errors
126+
end
127+
128+
def transaction_errors
129+
Array(response.body['errors']).map do |error|
130+
ResponseError.new(error['message'], error['status'], error['code'])
131+
end
132+
end
133+
134+
def non_transaction_errors
135+
return [] unless response.status == 400
136+
Array(ResponseError.new(response.body['message'], response.body['exception'], response.body['fullname']))
137+
end
138+
139+
def error
140+
errors.first
141+
end
142+
143+
def error_msg
144+
error && error.message
145+
end
146+
147+
def error_status
148+
error && error.status
149+
end
150+
151+
def error_code
152+
error && error.code
153+
end
154+
124155
def error?
125-
!!@error
156+
errors.any?
157+
end
158+
159+
def uncommited?
160+
@uncommited
126161
end
127162

128163
def data?
@@ -147,55 +182,45 @@ def set_data(data, columns)
147182
@struct = columns.empty? ? Object.new : Struct.new(*columns.map(&:to_sym))
148183
self
149184
end
150-
151-
def set_error(error_msg, error_status, error_core)
152-
@error = true
153-
@error_msg = error_msg
154-
@error_status = error_status
155-
@error_code = error_core
156-
self
185+
186+
def set_data_from_request
187+
return if error?
188+
if transaction_response? && response.body['results']
189+
set_data(response.body['results'][0]['data'], response.body['results'][0]['columns'])
190+
else
191+
set_data(response.body['data'], response.body['columns'])
192+
end
157193
end
158-
194+
159195
def raise_error
160-
fail 'Tried to raise error without an error' unless @error
161-
fail ResponseError.new(@error_msg, @error_status, @error_code)
196+
fail 'Tried to raise error without an error' unless error?
197+
fail error
162198
end
163-
199+
164200
def raise_cypher_error
165-
fail 'Tried to raise error without an error' unless @error
166-
fail Neo4j::Session::CypherError.new(@error_msg, @error_code, @error_status)
201+
fail 'Tried to raise error without an error' unless error?
202+
fail Neo4j::Session::CypherError.new(error.message, error.code, error.status)
167203
end
168-
169-
204+
170205
def self.create_with_no_tx(response)
171-
case response.status
172-
when 200
173-
CypherResponse.new(response).set_data(response.body['data'], response.body['columns'])
174-
when 400
175-
CypherResponse.new(response).set_error(response.body['message'], response.body['exception'], response.body['fullname'])
176-
else
177-
fail "Unknown response code #{response.status} for #{response.env[:url]}"
178-
end
206+
CypherResponse.new(response)
179207
end
180208

181209
def self.create_with_tx(response)
182-
fail "Unknown response code #{response.status} for #{response.request_uri}" unless response.status == 200
183-
184-
first_result = response.body['results'][0]
185-
cr = CypherResponse.new(response, true)
186-
187-
if response.body['errors'].empty?
188-
cr.set_data(first_result['data'], first_result['columns'])
189-
else
190-
first_error = response.body['errors'].first
191-
cr.set_error(first_error['message'], first_error['status'], first_error['code'])
192-
end
193-
cr
210+
CypherResponse.new(response, true)
194211
end
195212

196213
def transaction_response?
197214
response.respond_to?('body') && !response.body['commit'].nil?
198215
end
216+
217+
def transaction_failed?
218+
errors.any? { |e| e.code =~ /Neo\.DatabaseError/ }
219+
end
220+
221+
def transaction_not_found?
222+
errors.any? { |e| e.code == 'Neo.ClientError.Transaction.UnknownId' }
223+
end
199224

200225
def rest_data
201226
@result_index = @row_index = 0

lib/neo4j-server/cypher_transaction.rb

+62-19
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,34 @@ def initialize(url, session_connection)
2222

2323
ROW_REST = %w(row REST)
2424
def _query(cypher_query, params = nil)
25-
fail 'Transaction expired, unable to perform query' if expired?
2625
statement = {statement: cypher_query, parameters: params, resultDataContents: ROW_REST}
2726
body = {statements: [statement]}
2827

2928
response = exec_url && commit_url ? connection.post(exec_url, body) : register_urls(body)
30-
_create_cypher_response(response)
29+
_create_cypher_response(response).tap do |cypher_response|
30+
handle_transaction_errors(cypher_response)
31+
end
32+
end
33+
34+
def _create_cypher_response(response)
35+
CypherResponse.create_with_tx(response)
36+
end
37+
38+
# Replaces current transaction with invalid transaction indicating it was rolled back or expired on the server side. http://neo4j.com/docs/stable/status-codes.html#_classifications
39+
def handle_transaction_errors(response)
40+
tx_class = if response.transaction_not_found?
41+
ExpiredCypherTransaction
42+
elsif response.transaction_failed?
43+
InvalidCypherTransaction
44+
end
45+
46+
register_invalid_transaction(tx_class) if tx_class
47+
end
48+
49+
def register_invalid_transaction(tx_class)
50+
tx = tx_class.new(Neo4j::Transaction.current)
51+
Neo4j::Transaction.unregister_current
52+
tx.register_instance
3153
end
3254

3355
def _delete_tx
@@ -36,7 +58,7 @@ def _delete_tx
3658

3759
def _commit_tx
3860
_tx_query(:post, commit_url, nil)
39-
end
61+
end
4062

4163
private
4264

@@ -57,23 +79,44 @@ def register_urls(body)
5779
response
5880
end
5981

60-
def _create_cypher_response(response)
61-
first_result = response.body['results'][0]
62-
63-
cr = CypherResponse.new(response, true)
64-
if response.body['errors'].empty?
65-
cr.set_data(first_result['data'], first_result['columns'])
66-
else
67-
first_error = response.body['errors'].first
68-
expired if first_error['message'].match(/Unrecognized transaction id/)
69-
cr.set_error(first_error['message'], first_error['code'], first_error['code'])
70-
end
71-
cr
72-
end
73-
7482
def empty_response
7583
OpenStruct.new(status: 200, body: '')
7684
end
85+
86+
def valid?
87+
!invalid?
88+
end
89+
90+
def expired?
91+
is_a? ExpiredCypherTransaction
92+
end
93+
94+
def invalid?
95+
is_a? InvalidCypherTransaction
96+
end
97+
end
98+
99+
class InvalidCypherTransaction < CypherTransaction
100+
attr_accessor :original_transaction
101+
102+
def initialize(transaction)
103+
self.original_transaction = transaction
104+
mark_failed
105+
end
106+
107+
def close
108+
Neo4j::Transaction.unregister(self)
109+
end
110+
111+
def _query(cypher_query, params=nil)
112+
fail 'Transaction invalid, unable to perform query'
113+
end
114+
end
115+
116+
class ExpiredCypherTransaction < InvalidCypherTransaction
117+
def _query(cypher_query, params=nil)
118+
fail 'Transaction expired, unable to perform query'
119+
end
77120
end
78-
end
79-
end
121+
end
122+
end

lib/neo4j/transaction.rb

+1-10
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def register_instance
99
Neo4j::Transaction.register(self)
1010
end
1111

12-
# Marks this transaction as failed, which means that it will unconditionally be rolled back when close() is called. Aliased for legacy purposes.
12+
# Marks this transaction as failed on the client side, which means that it will unconditionally be rolled back when close() is called. Aliased for legacy purposes.
1313
def mark_failed
1414
@failure = true
1515
end
@@ -21,15 +21,6 @@ def failed?
2121
end
2222
alias_method :failure?, :failed?
2323

24-
def mark_expired
25-
@expired = true
26-
end
27-
alias_method :expired, :mark_expired
28-
29-
def expired?
30-
!!@expired
31-
end
32-
3324
# @private
3425
def push_nested!
3526
@pushed_nested += 1

spec/neo4j-server/e2e/cypher_transaction_spec.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ module Server
4646
expect(r.error?).to be true
4747

4848
expect(r.error_msg).to match(/Invalid input/)
49-
expect(r.error_status).to match(/Syntax/)
49+
expect(r.error_code).to match(/Syntax/)
5050
end
5151

5252
it 'can rollback' do
@@ -73,7 +73,7 @@ module Server
7373
it 'cannot continue operations if a transaction is expired' do
7474
node = Neo4j::Node.create(name: 'andreas')
7575
Neo4j::Transaction.run do |tx|
76-
tx.expired
76+
tx.register_invalid_transaction(Neo4j::Server::ExpiredCypherTransaction)
7777
expect { node[:name] = 'foo' }.to raise_error 'Transaction expired, unable to perform query'
7878
end
7979
end

spec/neo4j-server/unit/cypher_response_unit_spec.rb

+24
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,30 @@ def successful_response(response)
161161
end
162162

163163
skip 'returns hydrated CypherPath objects?'
164+
165+
describe '#errors' do
166+
let(:cypher_response) { CypherResponse.new(response, true) }
167+
168+
context 'without transaction' do
169+
let(:response) do
170+
double('tx_response', status: 400, body: {'message' => 'Some error', 'exception' => 'SomeError', 'fullname' => 'SomeError'})
171+
end
172+
173+
it 'returns an array of errors' do
174+
expect(cypher_response.errors).to be_a(Array)
175+
end
176+
end
177+
178+
context 'using transaction' do
179+
let(:response) do
180+
double('non-tx_response', status: 200, body: {'errors' => ['message' => 'Some error', 'status' => 'SomeError', 'code' => 'SomeError'], 'commit' => 'commit_uri'})
181+
end
182+
183+
it 'returns an array of errors' do
184+
expect(cypher_response.errors).to be_a(Array)
185+
end
186+
end
187+
end
164188
end
165189
end
166190
end

0 commit comments

Comments
 (0)