neo4j-core 6.1.6 → 7.0.0.alpha.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -9
  3. data/README.md +48 -0
  4. data/lib/neo4j-core.rb +23 -0
  5. data/lib/neo4j-core/helpers.rb +8 -0
  6. data/lib/neo4j-core/query.rb +23 -20
  7. data/lib/neo4j-core/query_clauses.rb +18 -32
  8. data/lib/neo4j-core/query_find_in_batches.rb +3 -1
  9. data/lib/neo4j-core/version.rb +1 -1
  10. data/lib/neo4j-embedded/cypher_response.rb +4 -0
  11. data/lib/neo4j-embedded/embedded_database.rb +3 -5
  12. data/lib/neo4j-embedded/embedded_node.rb +4 -4
  13. data/lib/neo4j-embedded/embedded_session.rb +21 -10
  14. data/lib/neo4j-embedded/embedded_transaction.rb +4 -10
  15. data/lib/neo4j-server/cypher_node.rb +5 -4
  16. data/lib/neo4j-server/cypher_relationship.rb +3 -3
  17. data/lib/neo4j-server/cypher_response.rb +4 -0
  18. data/lib/neo4j-server/cypher_session.rb +31 -22
  19. data/lib/neo4j-server/cypher_transaction.rb +23 -15
  20. data/lib/neo4j-server/resource.rb +3 -4
  21. data/lib/neo4j/core/cypher_session.rb +17 -9
  22. data/lib/neo4j/core/cypher_session/adaptors.rb +116 -33
  23. data/lib/neo4j/core/cypher_session/adaptors/bolt.rb +331 -0
  24. data/lib/neo4j/core/cypher_session/adaptors/bolt/chunk_writer_io.rb +76 -0
  25. data/lib/neo4j/core/cypher_session/adaptors/bolt/pack_stream.rb +288 -0
  26. data/lib/neo4j/core/cypher_session/adaptors/embedded.rb +60 -29
  27. data/lib/neo4j/core/cypher_session/adaptors/has_uri.rb +63 -0
  28. data/lib/neo4j/core/cypher_session/adaptors/http.rb +123 -119
  29. data/lib/neo4j/core/cypher_session/responses.rb +17 -2
  30. data/lib/neo4j/core/cypher_session/responses/bolt.rb +135 -0
  31. data/lib/neo4j/core/cypher_session/responses/embedded.rb +46 -11
  32. data/lib/neo4j/core/cypher_session/responses/http.rb +49 -40
  33. data/lib/neo4j/core/cypher_session/transactions.rb +33 -0
  34. data/lib/neo4j/core/cypher_session/transactions/bolt.rb +36 -0
  35. data/lib/neo4j/core/cypher_session/transactions/embedded.rb +32 -0
  36. data/lib/neo4j/core/cypher_session/transactions/http.rb +52 -0
  37. data/lib/neo4j/core/instrumentable.rb +2 -2
  38. data/lib/neo4j/core/label.rb +182 -0
  39. data/lib/neo4j/core/node.rb +8 -3
  40. data/lib/neo4j/core/relationship.rb +12 -4
  41. data/lib/neo4j/entity_equality.rb +1 -1
  42. data/lib/neo4j/session.rb +4 -5
  43. data/lib/neo4j/transaction.rb +108 -72
  44. data/neo4j-core.gemspec +6 -6
  45. metadata +34 -40
@@ -0,0 +1,32 @@
1
+ require 'neo4j/core/cypher_session/transactions'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Transactions
7
+ class Embedded < Base
8
+ def initialize(*args)
9
+ super
10
+ @java_tx = adaptor.graph_db.begin_tx
11
+ end
12
+
13
+ def commit
14
+ return if !@java_tx
15
+
16
+ @java_tx.success
17
+ @java_tx.close
18
+ rescue Java::OrgNeo4jGraphdb::TransactionFailureException => e
19
+ raise CypherError, e.message
20
+ end
21
+
22
+ def delete
23
+ return if !@java_tx
24
+
25
+ @java_tx.failure
26
+ @java_tx.close
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,52 @@
1
+ require 'neo4j/core/cypher_session/transactions'
2
+
3
+ module Neo4j
4
+ module Core
5
+ class CypherSession
6
+ module Transactions
7
+ class HTTP < Base
8
+ # Should perhaps have transaction adaptors only define #close
9
+ # commit/delete are, I think, an implementation detail
10
+
11
+ def commit
12
+ adaptor.requestor.request(:post, query_path(true)) if started?
13
+ end
14
+
15
+ def delete
16
+ adaptor.requestor.request(:delete, query_path) if started?
17
+ end
18
+
19
+ def query_path(commit = false)
20
+ if id
21
+ "/db/data/transaction/#{id}"
22
+ else
23
+ '/db/data/transaction'
24
+ end.tap do |path|
25
+ path << '/commit' if commit
26
+ end
27
+ end
28
+
29
+ # Takes the transaction URL from Neo4j and parses out the ID
30
+ def apply_id_from_url!(url)
31
+ root.instance_variable_set('@id', url.match(%r{/(\d+)/?$})[1].to_i) if url
32
+ # @id = url.match(%r{/(\d+)/?$})[1].to_i if url
33
+ end
34
+
35
+ def started?
36
+ !!id
37
+ end
38
+
39
+ def id
40
+ root.instance_variable_get('@id')
41
+ end
42
+
43
+ private
44
+
45
+ def connection
46
+ adaptor.connection
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -12,13 +12,13 @@ module Neo4j
12
12
  end
13
13
 
14
14
  module ClassMethods
15
- def instrument(name, label, arguments, &block)
15
+ def instrument(name, label, arguments)
16
16
  # defining class methods
17
17
  klass = class << self; self; end
18
18
  klass.instance_eval do
19
19
  define_method("subscribe_to_#{name}") do |&b|
20
20
  ActiveSupport::Notifications.subscribe(label) do |a, start, finish, id, payload|
21
- b.call block.call(a, start, finish, id, payload)
21
+ b.call yield(a, start, finish, id, payload)
22
22
  end
23
23
  end
24
24
 
@@ -0,0 +1,182 @@
1
+ module Neo4j
2
+ module Core
3
+ class Label
4
+ attr_reader :name
5
+
6
+ def initialize(name, session)
7
+ @name = name
8
+ @session = session
9
+ end
10
+
11
+ def create_index(property, options = {})
12
+ validate_index_options!(options)
13
+ properties = property.is_a?(Array) ? property.join(',') : property
14
+ schema_query("CREATE INDEX ON :`#{@name}`(#{properties})")
15
+ end
16
+
17
+ def drop_index(property, options = {})
18
+ validate_index_options!(options)
19
+ schema_query("DROP INDEX ON :`#{@name}`(#{property})")
20
+ end
21
+
22
+ # Creates a neo4j constraint on a property
23
+ # See http://docs.neo4j.org/chunked/stable/query-constraints.html
24
+ # @example
25
+ # label = Neo4j::Label.create(:person, session)
26
+ # label.create_constraint(:name, {type: :unique}, session)
27
+ #
28
+ def create_constraint(property, constraints)
29
+ cypher = case constraints[:type]
30
+ when :unique, :uniqueness
31
+ "CREATE CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
32
+ else
33
+ fail "Not supported constraint #{constraints.inspect} for property #{property} (expected :type => :unique)"
34
+ end
35
+ schema_query(cypher)
36
+ end
37
+
38
+ def create_uniqueness_constraint(property, options = {})
39
+ create_constraint(property, options.merge(type: :unique))
40
+ end
41
+
42
+ # Drops a neo4j constraint on a property
43
+ # See http://docs.neo4j.org/chunked/stable/query-constraints.html
44
+ # @example
45
+ # label = Neo4j::Label.create(:person, session)
46
+ # label.create_constraint(:name, {type: :unique}, session)
47
+ # label.drop_constraint(:name, {type: :unique}, session)
48
+ #
49
+ def drop_constraint(property, constraint)
50
+ cypher = case constraint[:type]
51
+ when :unique, :uniqueness
52
+ "DROP CONSTRAINT ON (n:`#{name}`) ASSERT n.`#{property}` IS UNIQUE"
53
+ else
54
+ fail "Not supported constraint #{constraint.inspect}"
55
+ end
56
+ schema_query(cypher)
57
+ end
58
+
59
+ def drop_uniqueness_constraint(property, options = {})
60
+ drop_constraint(property, options.merge(type: :unique))
61
+ end
62
+
63
+ def indexes
64
+ @session.indexes.select do |definition|
65
+ definition[:label] == @name.to_sym
66
+ end
67
+ end
68
+
69
+ def self.indexes_for(session)
70
+ session.indexes
71
+ end
72
+
73
+ def drop_indexes
74
+ indexes.each do |definition|
75
+ begin
76
+ @session.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})")
77
+ rescue Neo4j::Server::CypherResponse::ResponseError
78
+ # This will error on each constraint. Ignore and continue.
79
+ next
80
+ end
81
+ end
82
+ end
83
+
84
+ def self.drop_indexes_for(session)
85
+ indexes_for(session).each do |definition|
86
+ begin
87
+ session.query("DROP INDEX ON :`#{definition[:label]}`(#{definition[:properties][0]})")
88
+ rescue Neo4j::Server::CypherResponse::ResponseError
89
+ # This will error on each constraint. Ignore and continue.
90
+ next
91
+ end
92
+ end
93
+ end
94
+
95
+ def index?(property)
96
+ indexes.any? { |definition| definition[:properties] == [property.to_sym] }
97
+ end
98
+
99
+ def constraints(_options = {})
100
+ @session.constraints.select do |definition|
101
+ definition[:label] == @name.to_sym
102
+ end
103
+ end
104
+
105
+ def uniqueness_constraints(_options = {})
106
+ constraints.select do |definition|
107
+ definition[:type] == :uniqueness
108
+ end
109
+ end
110
+
111
+ def drop_uniqueness_constraints
112
+ uniqueness_constraints.each do |definition|
113
+ @session.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE")
114
+ end
115
+ end
116
+
117
+ def self.drop_uniqueness_constraints_for(session)
118
+ session.constraints.each do |definition|
119
+ session.query("DROP CONSTRAINT ON (n:`#{definition[:label]}`) ASSERT n.`#{definition[:properties][0]}` IS UNIQUE")
120
+ end
121
+ end
122
+
123
+ def constraint?(property)
124
+ constraints.any? { |definition| definition[:properties] == [property.to_sym] }
125
+ end
126
+
127
+ def uniqueness_constraint?(property)
128
+ uniqueness_constraints.include?([property])
129
+ end
130
+
131
+ def self.wait_for_schema_changes(session)
132
+ schema_threads(session).map(&:join)
133
+ set_schema_threads(session, [])
134
+ end
135
+
136
+ private
137
+
138
+ # Store schema threads on the session so that we can easily wait for all
139
+ # threads on a session regardless of label
140
+ def schema_threads
141
+ self.class.schema_threads(@session)
142
+ end
143
+
144
+ def schema_threads=(array)
145
+ self.class.set_schema_threads(@session, array)
146
+ end
147
+
148
+ class << self
149
+ def schema_threads(session)
150
+ session.instance_variable_get('@_schema_threads') || []
151
+ end
152
+
153
+ def set_schema_threads(session, array)
154
+ session.instance_variable_set('@_schema_threads', array)
155
+ end
156
+ end
157
+
158
+ # Schema queries can run separately from other queries, but they should
159
+ # be mutually exclusive to each other or we get locking errors
160
+ # SCHEMA_QUERY_SEMAPHORE = Mutex.new
161
+
162
+ # If there is a transaction going on, this could block
163
+ # So we run in a thread and it will go through at the next opportunity
164
+ def schema_query(cypher)
165
+ # Thread.new do
166
+ # SCHEMA_QUERY_SEMAPHORE.synchronize do
167
+
168
+ @session.transaction { |tx| tx.query(cypher, {}) }
169
+ rescue Exception => e # rubocop:disable Lint/RescueException
170
+ puts 'ERROR during schema query:', e.message, e.backtrace
171
+
172
+ # end
173
+ # end.tap { |thread| schema_threads << thread }
174
+ end
175
+
176
+ def validate_index_options!(options)
177
+ return unless options[:type] && options[:type] != :exact
178
+ fail "Type #{options[:type]} is not supported"
179
+ end
180
+ end
181
+ end
182
+ end
@@ -1,20 +1,25 @@
1
1
  require 'neo4j/core/wrappable'
2
+ require 'active_support/core_ext/hash/keys'
2
3
 
3
4
  module Neo4j
4
5
  module Core
5
6
  class Node
6
7
  attr_reader :id, :labels, :properties
7
- alias_method :props, :properties
8
+ alias props properties
8
9
 
9
10
  include Wrappable
10
11
 
11
12
  # Perhaps we should deprecate this?
12
- alias_method :neo_id, :id
13
+ alias neo_id id
13
14
 
14
15
  def initialize(id, labels, properties = {})
15
16
  @id = id
16
17
  @labels = labels.map(&:to_sym) unless labels.nil?
17
- @properties = properties
18
+ @properties = properties.symbolize_keys
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(Node) && neo_id == other.neo_id
18
23
  end
19
24
 
20
25
  class << self
@@ -1,14 +1,22 @@
1
+ require 'neo4j/core/wrappable'
2
+ require 'active_support/core_ext/hash/keys'
3
+
1
4
  module Neo4j
2
5
  module Core
3
6
  class Relationship
4
- attr_reader :id, :type, :properties
7
+ attr_reader :id, :type, :properties, :start_node_id, :end_node_id
8
+ alias props properties
9
+ alias neo_id id
10
+ alias rel_type type
5
11
 
6
12
  include Wrappable
7
13
 
8
- def initialize(id, type, properties)
14
+ def initialize(id, type, properties, start_node_id = nil, end_node_id = nil)
9
15
  @id = id
10
16
  @type = type.to_sym unless type.nil?
11
- @properties = properties
17
+ @properties = properties.symbolize_keys
18
+ @start_node_id = start_node_id
19
+ @end_node_id = end_node_id
12
20
  end
13
21
 
14
22
  class << self
@@ -17,7 +25,7 @@ module Neo4j
17
25
  type = nil # unknown
18
26
  properties = properties
19
27
 
20
- new(id, type, properties)
28
+ new(id, type, properties, nil, nil)
21
29
  end
22
30
  end
23
31
  end
@@ -3,6 +3,6 @@ module Neo4j
3
3
  def ==(other)
4
4
  other.class == self.class && other.neo_id == neo_id
5
5
  end
6
- alias_method :eql?, :==
6
+ alias eql? ==
7
7
  end
8
8
  end
data/lib/neo4j/session.rb CHANGED
@@ -36,11 +36,6 @@ module Neo4j
36
36
  true # TODO
37
37
  end
38
38
 
39
- # @abstract
40
- def begin_tx
41
- fail 'not impl.'
42
- end
43
-
44
39
  class CypherError < StandardError
45
40
  attr_reader :error_msg, :error_status, :error_code
46
41
  def initialize(error_msg, error_code, error_status)
@@ -77,6 +72,10 @@ module Neo4j
77
72
  fail 'not implemented'
78
73
  end
79
74
 
75
+ def transaction_class
76
+ self.class.transaction_class
77
+ end
78
+
80
79
  class << self
81
80
  # Creates a new session to Neo4j.
82
81
  # This will be the default session to be used unless there is already a session created (see #current and #set_current)
@@ -1,39 +1,86 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+ require 'active_support/per_thread_registry'
3
+
1
4
  module Neo4j
2
5
  module Transaction
3
6
  extend self
4
7
 
5
- module Instance
6
- # @private
7
- def register_instance
8
- @pushed_nested = 0
9
- Neo4j::Transaction.register(self)
8
+ # Provides a simple API to manage transactions for each session in a thread-safe manner
9
+ class TransactionsRegistry
10
+ extend ActiveSupport::PerThreadRegistry
11
+
12
+ attr_accessor :transactions_by_session_id
13
+ end
14
+
15
+ class Base
16
+ attr_reader :session, :root
17
+
18
+ def initialize(session, _options = {})
19
+ @session = session
20
+
21
+ Transaction.stack_for(session) << self
22
+
23
+ @root = Transaction.stack_for(session).first
24
+ # Neo4j::Core::Label::SCHEMA_QUERY_SEMAPHORE.lock if root?
25
+
26
+ # @parent = session_transaction_stack.last
27
+ # session_transaction_stack << self
10
28
  end
11
29
 
12
- # Marks this transaction as failed, which means that it will unconditionally be rolled back when close() is called. Aliased for legacy purposes.
13
- def mark_failed
14
- @failure = true
30
+ def inspect
31
+ status_string = [:id, :failed?, :active?, :commit_url].map do |method|
32
+ "#{method}: #{send(method)}" if respond_to?(method)
33
+ end.compact.join(', ')
34
+
35
+ "<#{self.class} [#{status_string}]"
15
36
  end
16
- alias_method :failure, :mark_failed
17
37
 
18
- # If it has been marked as failed. Aliased for legacy purposes.
19
- def failed?
20
- !!@failure
38
+ # Commits or marks this transaction for rollback, depending on whether #mark_failed has been previously invoked.
39
+ def close
40
+ tx_stack = Transaction.stack_for(@session)
41
+ fail 'Tried closing when transaction stack is empty (maybe you closed too many?)' if tx_stack.empty?
42
+ fail "Closed transaction which wasn't the most recent on the stack (maybe you forgot to close one?)" if tx_stack.pop != self
43
+
44
+ @closed = true
45
+
46
+ post_close! if tx_stack.empty?
47
+ end
48
+
49
+ def delete
50
+ fail 'not implemented'
51
+ end
52
+
53
+ def commit
54
+ fail 'not implemented'
21
55
  end
22
- alias_method :failure?, :failed?
23
56
 
24
57
  def autoclosed!
25
58
  @autoclosed = true if transient_failures_autoclose?
26
59
  end
27
60
 
28
- def transient_failures_autoclose?
29
- Neo4j::Session.current.version >= '2.2.6'
61
+ def closed?
62
+ !!@closed
30
63
  end
31
64
 
32
- def autoclosed?
33
- !!@autoclosed
65
+ # Marks this transaction as failed,
66
+ # which means that it will unconditionally be rolled back
67
+ # when #close is called.
68
+ # Aliased for legacy purposes.
69
+ def mark_failed
70
+ root.mark_failed if root && root != self
71
+ @failure = true
34
72
  end
73
+ alias failure mark_failed
74
+
75
+ # If it has been marked as failed.
76
+ # Aliased for legacy purposes.
77
+ def failed?
78
+ !!@failure
79
+ end
80
+ alias failure? failed?
35
81
 
36
82
  def mark_expired
83
+ @parent.mark_expired if @parent
37
84
  @expired = true
38
85
  end
39
86
 
@@ -41,43 +88,24 @@ module Neo4j
41
88
  !!@expired
42
89
  end
43
90
 
44
- # @private
45
- def push_nested!
46
- @pushed_nested += 1
91
+ def root?
92
+ @root == self
47
93
  end
48
94
 
49
- # @private
50
- def pop_nested!
51
- @pushed_nested -= 1
52
- end
95
+ private
53
96
 
54
- # Only for the embedded neo4j !
55
- # Acquires a read lock for entity for this transaction.
56
- # See neo4j java docs.
57
- # @param [Neo4j::Node,Neo4j::Relationship] entity
58
- # @return [Java::OrgNeo4jKernelImplCoreapi::PropertyContainerLocker]
59
- def acquire_read_lock(entity)
97
+ def transient_failures_autoclose?
98
+ Gem::Version.new(@session.version) >= Gem::Version.new('2.2.6')
60
99
  end
61
100
 
62
- # Only for the embedded neo4j !
63
- # Acquires a write lock for entity for this transaction.
64
- # See neo4j java docs.
65
- # @param [Neo4j::Node,Neo4j::Relationship] entity
66
- # @return [Java::OrgNeo4jKernelImplCoreapi::PropertyContainerLocker]
67
- def acquire_write_lock(entity)
101
+ def autoclosed?
102
+ !!@autoclosed
68
103
  end
69
104
 
70
- # Commits or marks this transaction for rollback, depending on whether failure() has been previously invoked.
71
- def close
72
- pop_nested!
73
- return if @pushed_nested >= 0
74
- fail "Can't commit transaction, already committed" if @pushed_nested < -1
75
- Neo4j::Transaction.unregister(self)
76
- post_close!
105
+ def active?
106
+ !closed?
77
107
  end
78
108
 
79
- private
80
-
81
109
  def post_close!
82
110
  return if autoclosed?
83
111
  if failed?
@@ -89,56 +117,64 @@ module Neo4j
89
117
  end
90
118
 
91
119
  # @return [Neo4j::Transaction::Instance]
92
- def new(current = Session.current!)
93
- current.begin_tx
120
+ def new(session = Session.current!)
121
+ session.transaction
94
122
  end
95
123
 
96
124
  # Runs the given block in a new transaction.
97
125
  # @param [Boolean] run_in_tx if true a new transaction will not be created, instead if will simply yield to the given block
98
126
  # @@yield [Neo4j::Transaction::Instance]
99
- def run(run_in_tx = true)
127
+ def run(*args)
128
+ session, run_in_tx = session_and_run_in_tx_from_args(args)
129
+
100
130
  fail ArgumentError, 'Expected a block to run in Transaction.run' unless block_given?
101
131
 
102
132
  return yield(nil) unless run_in_tx
103
133
 
104
- tx = Neo4j::Transaction.new
134
+ tx = Neo4j::Transaction.new(session)
105
135
  yield tx
106
136
  rescue Exception => e # rubocop:disable Lint/RescueException
107
- print_exception_cause(e)
137
+ # print_exception_cause(e)
138
+
108
139
  tx.mark_failed unless tx.nil?
109
- raise
140
+ raise e
110
141
  ensure
111
142
  tx.close unless tx.nil?
112
143
  end
113
144
 
114
- # @return [Neo4j::Transaction]
115
- def current
116
- Thread.current[:neo4j_curr_tx]
117
- end
118
-
119
- # @private
120
- def print_exception_cause(exception)
121
- return if !exception.respond_to?(:cause) || !exception.cause.respond_to?(:print_stack_trace)
145
+ # To support old syntax of providing run_in_tx first
146
+ # But session first is ideal
147
+ def session_and_run_in_tx_from_args(args)
148
+ fail ArgumentError, 'Too many arguments' if args.size > 2
149
+
150
+ if args.empty?
151
+ [Session.current!, true]
152
+ else
153
+ result = args.dup
154
+ if result.size == 1
155
+ result << ([true, false].include?(args[0]) ? Session.current! : true)
156
+ end
122
157
 
123
- puts "Java Exception in a transaction, cause: #{exception.cause}"
124
- exception.cause.print_stack_trace
158
+ [true, false].include?(result[0]) ? result.reverse : result
159
+ end
125
160
  end
126
161
 
127
- # @private
128
- def unregister(tx)
129
- Thread.current[:neo4j_curr_tx] = nil if tx == Thread.current[:neo4j_curr_tx]
162
+ def current_for(session)
163
+ stack_for(session).first
130
164
  end
131
165
 
132
- # @private
133
- def register(tx)
134
- # we don't support running more then one transaction per thread
135
- fail 'Already running a transaction' if current
136
- Thread.current[:neo4j_curr_tx] = tx
166
+ def stack_for(session)
167
+ TransactionsRegistry.transactions_by_session_id ||= {}
168
+ TransactionsRegistry.transactions_by_session_id[session.object_id] ||= []
137
169
  end
138
170
 
139
- # @private
140
- def unregister_current
141
- Thread.current[:neo4j_curr_tx] = nil
171
+ private
172
+
173
+ def print_exception_cause(exception)
174
+ return if !exception.respond_to?(:cause) || !exception.cause.respond_to?(:print_stack_trace)
175
+
176
+ Core.logger.info "Java Exception in a transaction, cause: #{exception.cause}"
177
+ exception.cause.print_stack_trace
142
178
  end
143
179
  end
144
180
  end