mongo 1.4.0 → 1.5.0.rc0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/docs/HISTORY.md +15 -0
  2. data/docs/REPLICA_SETS.md +19 -7
  3. data/lib/mongo.rb +1 -0
  4. data/lib/mongo/collection.rb +1 -1
  5. data/lib/mongo/connection.rb +29 -351
  6. data/lib/mongo/cursor.rb +88 -6
  7. data/lib/mongo/gridfs/grid.rb +4 -2
  8. data/lib/mongo/gridfs/grid_file_system.rb +4 -2
  9. data/lib/mongo/networking.rb +345 -0
  10. data/lib/mongo/repl_set_connection.rb +236 -191
  11. data/lib/mongo/util/core_ext.rb +45 -0
  12. data/lib/mongo/util/logging.rb +5 -0
  13. data/lib/mongo/util/node.rb +6 -4
  14. data/lib/mongo/util/pool.rb +73 -26
  15. data/lib/mongo/util/pool_manager.rb +100 -30
  16. data/lib/mongo/util/uri_parser.rb +29 -21
  17. data/lib/mongo/version.rb +1 -1
  18. data/test/bson/binary_test.rb +6 -8
  19. data/test/bson/bson_test.rb +1 -0
  20. data/test/bson/ordered_hash_test.rb +2 -0
  21. data/test/bson/test_helper.rb +0 -17
  22. data/test/collection_test.rb +22 -0
  23. data/test/connection_test.rb +1 -1
  24. data/test/cursor_test.rb +3 -3
  25. data/test/load/thin/load.rb +4 -7
  26. data/test/replica_sets/basic_test.rb +46 -0
  27. data/test/replica_sets/connect_test.rb +35 -58
  28. data/test/replica_sets/count_test.rb +15 -6
  29. data/test/replica_sets/insert_test.rb +6 -7
  30. data/test/replica_sets/query_test.rb +4 -6
  31. data/test/replica_sets/read_preference_test.rb +112 -8
  32. data/test/replica_sets/refresh_test.rb +66 -36
  33. data/test/replica_sets/refresh_with_threads_test.rb +55 -0
  34. data/test/replica_sets/replication_ack_test.rb +3 -6
  35. data/test/replica_sets/rs_test_helper.rb +12 -6
  36. data/test/replica_sets/threading_test.rb +111 -0
  37. data/test/test_helper.rb +9 -2
  38. data/test/threading_test.rb +14 -6
  39. data/test/tools/repl_set_manager.rb +55 -40
  40. data/test/unit/collection_test.rb +2 -1
  41. data/test/unit/connection_test.rb +8 -8
  42. data/test/unit/grid_test.rb +4 -2
  43. data/test/unit/pool_manager_test.rb +1 -0
  44. data/test/unit/read_test.rb +17 -5
  45. data/test/uri_test.rb +9 -4
  46. metadata +13 -28
  47. data/test/replica_sets/connection_string_test.rb +0 -29
  48. data/test/replica_sets/pooled_insert_test.rb +0 -58
  49. data/test/replica_sets/query_secondaries.rb +0 -109
@@ -20,7 +20,7 @@ module Mongo
20
20
  class URIParser
21
21
 
22
22
  DEFAULT_PORT = 27017
23
- MONGODB_URI_MATCHER = /(([-.\w]+):([^@]+)@)?([-.\w]+)(:([\w]+))?(\/([-\w]+))?/
23
+ MONGODB_URI_MATCHER = /(([-.\w:]+):([^@,]+)@)?((?:(?:[-.\w]+)(?::(?:[\w]+))?,?)+)(\/([-\w]+))?/
24
24
  MONGODB_URI_SPEC = "mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/database]"
25
25
  SPEC_ATTRS = [:nodes, :auths]
26
26
  OPT_ATTRS = [:connect, :replicaset, :slaveok, :safe, :w, :wtimeout, :fsync]
@@ -108,35 +108,43 @@ module Mongo
108
108
 
109
109
  private
110
110
 
111
- def parse_hosts(hosts)
111
+ def parse_hosts(uri_without_proto)
112
112
  @nodes = []
113
113
  @auths = []
114
- specs = hosts.split(',')
115
- specs.each do |spec|
116
- matches = MONGODB_URI_MATCHER.match(spec)
117
- if !matches
118
- raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
119
- end
120
114
 
121
- uname = matches[2]
122
- pwd = matches[3]
123
- host = matches[4]
124
- port = matches[6] || DEFAULT_PORT
115
+ matches = MONGODB_URI_MATCHER.match(uri_without_proto)
116
+
117
+ if !matches
118
+ raise MongoArgumentError, "MongoDB URI must match this spec: #{MONGODB_URI_SPEC}"
119
+ end
120
+
121
+ uname = matches[2]
122
+ pwd = matches[3]
123
+ hosturis = matches[4].split(',')
124
+ db = matches[6]
125
+
126
+ hosturis.each do |hosturi|
127
+ # If port is present, use it, otherwise use default port
128
+ host, port = hosturi.split(':') + [DEFAULT_PORT]
129
+
125
130
  if !(port.to_s =~ /^\d+$/)
126
131
  raise MongoArgumentError, "Invalid port #{port}; port must be specified as digits."
127
132
  end
128
- port = port.to_i
129
- db = matches[8]
130
-
131
- if uname && pwd && db
132
- auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
133
- elsif uname || pwd || db
134
- raise MongoArgumentError, "MongoDB URI must include all three of username, password, " +
135
- "and db if any one of these is specified."
136
- end
133
+
134
+ port = port.to_i
137
135
 
138
136
  @nodes << [host, port]
139
137
  end
138
+
139
+ if uname && pwd && db
140
+ auths << {'db_name' => db, 'username' => uname, 'password' => pwd}
141
+ elsif uname || pwd
142
+ raise MongoArgumentError, "MongoDB URI must include username, password, "
143
+ "and db if username and password are specified."
144
+ end
145
+
146
+ # The auths are repeated for each host in a replica set
147
+ @auths *= hosturis.length
140
148
  end
141
149
 
142
150
  # This method uses the lambdas defined in OPT_VALID and OPT_CONV to validate
@@ -1,3 +1,3 @@
1
1
  module Mongo
2
- VERSION = "1.4.0"
2
+ VERSION = "1.5.0.rc0"
3
3
  end
@@ -2,14 +2,12 @@
2
2
  require './test/bson/test_helper'
3
3
 
4
4
  class BinaryTest < Test::Unit::TestCase
5
- context "Inspecting" do
6
- setup do
7
- @data = ("THIS IS BINARY " * 50).unpack("c*")
8
- end
5
+ def setup
6
+ @data = ("THIS IS BINARY " * 50).unpack("c*")
7
+ end
9
8
 
10
- should "not display actual data" do
11
- binary = BSON::Binary.new(@data)
12
- assert_equal "<BSON::Binary:#{binary.object_id}>", binary.inspect
13
- end
9
+ def test_do_not_display_binary_data
10
+ binary = BSON::Binary.new(@data)
11
+ assert_equal "<BSON::Binary:#{binary.object_id}>", binary.inspect
14
12
  end
15
13
  end
@@ -9,6 +9,7 @@ end
9
9
  require 'bigdecimal'
10
10
 
11
11
  begin
12
+ require 'date'
12
13
  require 'tzinfo'
13
14
  require 'active_support/core_ext'
14
15
  Time.zone = "Pacific Time (US & Canada)"
@@ -212,6 +212,8 @@ class OrderedHashTest < Test::Unit::TestCase
212
212
  assert @oh.keys.include?('z')
213
213
  @oh.delete_if { |k,v| k == 'z' }
214
214
  assert !@oh.keys.include?('z')
215
+ @oh.delete_if { |k, v| v > 0 }
216
+ assert @oh.keys.empty?
215
217
  end
216
218
 
217
219
  def test_reject
@@ -10,23 +10,6 @@ def silently
10
10
  result
11
11
  end
12
12
 
13
- begin
14
- require 'rubygems' if RUBY_VERSION < "1.9.0" && !ENV['C_EXT']
15
- silently { require 'shoulda' }
16
- silently { require 'mocha' }
17
- rescue LoadError
18
- puts <<MSG
19
-
20
- This test suite requires shoulda and mocha.
21
- You can install them as follows:
22
- gem install shoulda
23
- gem install mocha
24
-
25
- MSG
26
-
27
- exit
28
- end
29
-
30
13
  require 'bson_ext/cbson' if !(RUBY_PLATFORM =~ /java/) && ENV['C_EXT']
31
14
 
32
15
  class Test::Unit::TestCase
@@ -301,6 +301,7 @@ class TestCollection < Test::Unit::TestCase
301
301
  @conn = standard_connection
302
302
  @db = @conn[MONGO_TEST_DB]
303
303
  @test = @db['test-safe-remove']
304
+ @test.remove
304
305
  @test.save({:a => 50})
305
306
  assert_equal 1, @test.remove({}, :safe => true)["n"]
306
307
  @test.drop
@@ -704,6 +705,26 @@ class TestCollection < Test::Unit::TestCase
704
705
  coll.ensure_index([['a', 1]])
705
706
  end
706
707
 
708
+
709
+ if @@version > '2.0.0'
710
+ def test_show_disk_loc
711
+ @@test.save({:a => 1})
712
+ @@test.save({:a => 2})
713
+ assert @@test.find({:a => 1}, :show_disk_loc => true).show_disk_loc
714
+ assert @@test.find({:a => 1}, :show_disk_loc => true).next['$diskLoc']
715
+ @@test.remove
716
+ end
717
+
718
+ def test_max_scan
719
+ 1000.times do |n|
720
+ @@test.save({:a => n})
721
+ end
722
+ assert @@test.find({:a => 999}).next
723
+ assert !@@test.find({:a => 999}, :max_scan => 500).next
724
+ @@test.remove
725
+ end
726
+ end
727
+
707
728
  context "Grouping" do
708
729
  setup do
709
730
  @@test.remove
@@ -782,6 +803,7 @@ class TestCollection < Test::Unit::TestCase
782
803
  context "A collection with two records" do
783
804
  setup do
784
805
  @collection = @@db.collection('test-collection')
806
+ @collection.remove
785
807
  @collection.insert({:name => "Jones"})
786
808
  @collection.insert({:name => "Smith"})
787
809
  end
@@ -318,7 +318,7 @@ class TestConnection < Test::Unit::TestCase
318
318
  TCPSocket.stubs(:new).returns(fake_socket)
319
319
 
320
320
  @con.primary_pool.checkout_new_socket
321
- assert_equal [], @con.primary_pool.close
321
+ assert @con.primary_pool.close
322
322
  end
323
323
  end
324
324
  end
@@ -53,8 +53,8 @@ class CursorTest < Test::Unit::TestCase
53
53
  def test_exhaust
54
54
  if @@version >= "2.0"
55
55
  @@coll.remove
56
- data = "1" * 100_000
57
- 10_000.times do |n|
56
+ data = "1" * 10_000
57
+ 5000.times do |n|
58
58
  @@coll.insert({:n => n, :data => data})
59
59
  end
60
60
 
@@ -65,7 +65,7 @@ class CursorTest < Test::Unit::TestCase
65
65
 
66
66
  c = Cursor.new(@@coll)
67
67
  c.add_option(OP_QUERY_EXHAUST)
68
- 9999.times do
68
+ 4999.times do
69
69
  c.next
70
70
  end
71
71
  assert c.has_next?
@@ -1,7 +1,7 @@
1
1
  require File.join(File.dirname(__FILE__), '..', '..', '..', 'lib', 'mongo')
2
2
  require 'logger'
3
3
 
4
- $con = Mongo::Connection.new
4
+ $con = Mongo::ReplSetConnection.new(['localhost', 30000], ['localhost', 30001], :read => :secondary, :refresh_mode => :sync, :refresh_interval => 30)
5
5
  $db = $con['foo']
6
6
 
7
7
  class Load < Sinatra::Base
@@ -13,12 +13,9 @@ class Load < Sinatra::Base
13
13
  end
14
14
 
15
15
  get '/' do
16
- 3.times do |n|
17
- if (v=$db.eval("1 + #{n}")) != 1 + n
18
- STDERR << "#{1 + n} expected but got #{v}"
19
- raise StandardError, "#{1 + n} expected but got #{v}"
20
- end
21
- end
16
+ $db['test'].insert({:a => rand(1000)})
17
+ $db['test'].find({:a => {'$gt' => rand(2)}}, :read => :secondary).limit(2).to_a
18
+ "ok"
22
19
  end
23
20
 
24
21
  end
@@ -0,0 +1,46 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require './test/replica_sets/rs_test_helper'
3
+
4
+ class BasicTest < Test::Unit::TestCase
5
+ include ReplicaSetTest
6
+
7
+ def teardown
8
+ self.rs.restart_killed_nodes
9
+ @conn.close if defined?(@conn) && @conn
10
+ end
11
+
12
+ def test_connect
13
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[1]], [self.rs.host, self.rs.ports[0]],
14
+ [self.rs.host, self.rs.ports[2]], :name => self.rs.name)
15
+ assert @conn.connected?
16
+
17
+ assert_equal self.rs.primary, @conn.primary
18
+ assert_equal self.rs.secondaries.sort, @conn.secondaries.sort
19
+ assert_equal self.rs.arbiters.sort, @conn.arbiters.sort
20
+
21
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[1]], [self.rs.host, self.rs.ports[0]],
22
+ :name => self.rs.name)
23
+ assert @conn.connected?
24
+ end
25
+
26
+ def test_accessors
27
+ seeds = [[self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
28
+ [self.rs.host, self.rs.ports[2]]]
29
+ args = seeds << {:name => self.rs.name}
30
+ @conn = ReplSetConnection.new(*args)
31
+
32
+ assert_equal @conn.host, self.rs.primary[0]
33
+ assert_equal @conn.port, self.rs.primary[1]
34
+ assert_equal @conn.host, @conn.primary_pool.host
35
+ assert_equal @conn.port, @conn.primary_pool.port
36
+ assert_equal @conn.nodes.sort, @conn.seeds.sort
37
+ assert_equal 2, @conn.secondaries.length
38
+ assert_equal 0, @conn.arbiters.length
39
+ assert_equal 2, @conn.secondary_pools.length
40
+ assert_equal self.rs.name, @conn.replica_set_name
41
+ assert @conn.secondary_pools.include?(@conn.read_pool)
42
+ assert_equal 5, @conn.tag_map.keys.length
43
+ assert_equal 90, @conn.refresh_interval
44
+ assert_equal @conn.refresh_mode, false
45
+ end
46
+ end
@@ -1,111 +1,88 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  require './test/replica_sets/rs_test_helper'
3
3
 
4
- # NOTE: This test expects a replica set of three nodes to be running on RS.host,
5
- # on ports TEST_PORT, RS.ports[1], and TEST + 2.
6
4
  class ConnectTest < Test::Unit::TestCase
7
- include Mongo
5
+ include ReplicaSetTest
8
6
 
9
7
  def teardown
10
- RS.restart_killed_nodes
8
+ self.rs.restart_killed_nodes
11
9
  @conn.close if defined?(@conn) && @conn
12
10
  end
13
11
 
12
+ # TODO: test connect timeout.
13
+
14
14
  def test_connect_with_deprecated_multi
15
- @conn = Connection.multi([[RS.host, RS.ports[0]], [RS.host, RS.ports[1]]], :name => RS.name)
15
+ @conn = Connection.multi([[self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]]], :name => self.rs.name)
16
16
  assert @conn.is_a?(ReplSetConnection)
17
17
  assert @conn.connected?
18
18
  end
19
19
 
20
20
  def test_connect_bad_name
21
21
  assert_raise_error(ReplicaSetConnectionError, "-wrong") do
22
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
23
- [RS.host, RS.ports[2]], :name => RS.name + "-wrong")
22
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
23
+ [self.rs.host, self.rs.ports[2]], :name => self.rs.name + "-wrong")
24
24
  end
25
25
  end
26
26
 
27
- # def test_connect_timeout
28
- # passed = false
29
- # timeout = 3
30
- # begin
31
- # t0 = Time.now
32
- # @conn = ReplSetConnection.new(['192.169.169.1', 27017], :connect_timeout => timeout)
33
- # rescue OperationTimeout
34
- # passed = true
35
- # t1 = Time.now
36
- # end
37
-
38
- # assert passed
39
- # assert t1 - t0 < timeout + 1
40
- # end
41
-
42
- def test_connect
43
- @conn = ReplSetConnection.new([RS.host, RS.ports[1]], [RS.host, RS.ports[0]],
44
- [RS.host, RS.ports[2]], :name => RS.name)
45
- assert @conn.connected?
46
-
47
- assert_equal RS.primary, @conn.primary
48
- assert_equal RS.secondaries.sort, @conn.secondaries.sort
49
- assert_equal RS.arbiters.sort, @conn.arbiters.sort
50
-
51
- @conn = ReplSetConnection.new([RS.host, RS.ports[1]], [RS.host, RS.ports[0]],
52
- :name => RS.name)
53
- assert @conn.connected?
54
- end
55
-
56
- def test_host_port_accessors
57
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
58
- [RS.host, RS.ports[2]], :name => RS.name)
59
-
60
- assert_equal @conn.host, RS.primary[0]
61
- assert_equal @conn.port, RS.primary[1]
62
- end
63
-
64
27
  def test_connect_with_primary_node_killed
65
- node = RS.kill_primary
28
+ node = self.rs.kill_primary
66
29
 
67
30
  # Becuase we're killing the primary and trying to connect right away,
68
31
  # this is going to fail right away.
69
32
  assert_raise_error(ConnectionFailure, "Failed to connect to primary node") do
70
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
71
- [RS.host, RS.ports[2]])
33
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
34
+ [self.rs.host, self.rs.ports[2]])
72
35
  end
73
36
 
74
37
  # This allows the secondary to come up as a primary
75
38
  rescue_connection_failure do
76
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
77
- [RS.host, RS.ports[2]])
39
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
40
+ [self.rs.host, self.rs.ports[2]])
78
41
  end
79
42
  end
80
43
 
81
44
  def test_connect_with_secondary_node_killed
82
- node = RS.kill_secondary
45
+ node = self.rs.kill_secondary
83
46
 
84
47
  rescue_connection_failure do
85
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
86
- [RS.host, RS.ports[2]])
48
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
49
+ [self.rs.host, self.rs.ports[2]])
87
50
  end
88
51
  assert @conn.connected?
89
52
  end
90
53
 
91
54
  def test_connect_with_third_node_killed
92
- RS.kill(RS.get_node_from_port(RS.ports[2]))
55
+ self.rs.kill(self.rs.get_node_from_port(self.rs.ports[2]))
93
56
 
94
57
  rescue_connection_failure do
95
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
96
- [RS.host, RS.ports[2]])
58
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
59
+ [self.rs.host, self.rs.ports[2]])
97
60
  end
98
61
  assert @conn.connected?
99
62
  end
100
63
 
101
64
  def test_connect_with_primary_stepped_down
102
- RS.step_down_primary
65
+ self.rs.step_down_primary
103
66
 
104
67
  rescue_connection_failure do
105
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]],
106
- [RS.host, RS.ports[2]])
68
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]], [self.rs.host, self.rs.ports[1]],
69
+ [self.rs.host, self.rs.ports[2]])
107
70
  end
108
71
  assert @conn.connected?
109
72
  end
110
73
 
74
+ def test_connect_with_connection_string
75
+ @conn = Connection.from_uri("mongodb://#{self.rs.host}:#{self.rs.ports[0]},#{self.rs.host}:#{self.rs.ports[1]}?replicaset=#{self.rs.name}")
76
+ assert @conn.is_a?(ReplSetConnection)
77
+ assert @conn.connected?
78
+ end
79
+
80
+ def test_connect_with_full_connection_string
81
+ @conn = Connection.from_uri("mongodb://#{self.rs.host}:#{self.rs.ports[0]},#{self.rs.host}:#{self.rs.ports[1]}?replicaset=#{self.rs.name};safe=true;w=2;fsync=true;slaveok=true")
82
+ assert @conn.is_a?(ReplSetConnection)
83
+ assert @conn.connected?
84
+ assert_equal 2, @conn.safe[:w]
85
+ assert @conn.safe[:fsync]
86
+ assert @conn.read_pool
87
+ end
111
88
  end
@@ -1,20 +1,22 @@
1
1
  $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  require './test/replica_sets/rs_test_helper'
3
3
 
4
- # NOTE: This test expects a replica set of three nodes to be running
5
- # on the local host.
6
4
  class ReplicaSetCountTest < Test::Unit::TestCase
7
- include Mongo
5
+ include ReplicaSetTest
8
6
 
9
7
  def setup
10
- @conn = ReplSetConnection.new([RS.host, RS.ports[0]], [RS.host, RS.ports[1]], [RS.host, RS.ports[2]])
8
+ @conn = ReplSetConnection.new([self.rs.host, self.rs.ports[0]],
9
+ [self.rs.host, self.rs.ports[1]], [self.rs.host, self.rs.ports[2]],
10
+ :read => :secondary)
11
+ assert @conn.primary_pool
12
+ @primary = Connection.new(@conn.primary_pool.host, @conn.primary_pool.port)
11
13
  @db = @conn.db(MONGO_TEST_DB)
12
14
  @db.drop_collection("test-sets")
13
15
  @coll = @db.collection("test-sets")
14
16
  end
15
17
 
16
18
  def teardown
17
- RS.restart_killed_nodes
19
+ self.rs.restart_killed_nodes
18
20
  @conn.close if @conn
19
21
  end
20
22
 
@@ -23,7 +25,7 @@ class ReplicaSetCountTest < Test::Unit::TestCase
23
25
  assert_equal 1, @coll.count
24
26
 
25
27
  # Kill the current master node
26
- @node = RS.kill_primary
28
+ @node = self.rs.kill_primary
27
29
 
28
30
  rescue_connection_failure do
29
31
  @coll.insert({:a => 30}, :safe => true)
@@ -33,4 +35,11 @@ class ReplicaSetCountTest < Test::Unit::TestCase
33
35
  assert_equal 3, @coll.count, "Second count failed"
34
36
  end
35
37
 
38
+ def test_count_command_sent_to_primary
39
+ @coll.insert({:a => 20}, :safe => {:w => 2, :wtimeout => 10000})
40
+ count_before = @primary['admin'].command({:serverStatus => 1})['opcounters']['command']
41
+ assert_equal 1, @coll.count
42
+ count_after = @primary['admin'].command({:serverStatus => 1})['opcounters']['command']
43
+ assert_equal 2, count_after - count_before
44
+ end
36
45
  end