redis 4.4.0 → 5.0.7

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/CHANGELOG.md +150 -0
  3. data/README.md +95 -160
  4. data/lib/redis/client.rb +84 -608
  5. data/lib/redis/commands/bitmaps.rb +66 -0
  6. data/lib/redis/commands/cluster.rb +28 -0
  7. data/lib/redis/commands/connection.rb +53 -0
  8. data/lib/redis/commands/geo.rb +84 -0
  9. data/lib/redis/commands/hashes.rb +254 -0
  10. data/lib/redis/commands/hyper_log_log.rb +37 -0
  11. data/lib/redis/commands/keys.rb +437 -0
  12. data/lib/redis/commands/lists.rb +339 -0
  13. data/lib/redis/commands/pubsub.rb +54 -0
  14. data/lib/redis/commands/scripting.rb +114 -0
  15. data/lib/redis/commands/server.rb +188 -0
  16. data/lib/redis/commands/sets.rb +214 -0
  17. data/lib/redis/commands/sorted_sets.rb +884 -0
  18. data/lib/redis/commands/streams.rb +402 -0
  19. data/lib/redis/commands/strings.rb +314 -0
  20. data/lib/redis/commands/transactions.rb +115 -0
  21. data/lib/redis/commands.rb +237 -0
  22. data/lib/redis/distributed.rb +208 -70
  23. data/lib/redis/errors.rb +15 -41
  24. data/lib/redis/hash_ring.rb +26 -26
  25. data/lib/redis/pipeline.rb +66 -120
  26. data/lib/redis/subscribe.rb +23 -15
  27. data/lib/redis/version.rb +1 -1
  28. data/lib/redis.rb +109 -3546
  29. metadata +27 -54
  30. data/lib/redis/cluster/command.rb +0 -81
  31. data/lib/redis/cluster/command_loader.rb +0 -34
  32. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  33. data/lib/redis/cluster/node.rb +0 -108
  34. data/lib/redis/cluster/node_key.rb +0 -31
  35. data/lib/redis/cluster/node_loader.rb +0 -37
  36. data/lib/redis/cluster/option.rb +0 -93
  37. data/lib/redis/cluster/slot.rb +0 -86
  38. data/lib/redis/cluster/slot_loader.rb +0 -49
  39. data/lib/redis/cluster.rb +0 -291
  40. data/lib/redis/connection/command_helper.rb +0 -39
  41. data/lib/redis/connection/hiredis.rb +0 -67
  42. data/lib/redis/connection/registry.rb +0 -13
  43. data/lib/redis/connection/ruby.rb +0 -427
  44. data/lib/redis/connection/synchrony.rb +0 -146
  45. data/lib/redis/connection.rb +0 -11
data/lib/redis/client.rb CHANGED
@@ -1,652 +1,128 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errors"
4
- require "socket"
5
- require "cgi"
3
+ require 'redis-client'
6
4
 
7
5
  class Redis
8
- class Client
9
- # Defaults are also used for converting string keys to symbols.
10
- DEFAULTS = {
11
- url: -> { ENV["REDIS_URL"] },
12
- scheme: "redis",
13
- host: "127.0.0.1",
14
- port: 6379,
15
- path: nil,
16
- read_timeout: nil,
17
- write_timeout: nil,
18
- connect_timeout: nil,
19
- timeout: 5.0,
20
- username: nil,
21
- password: nil,
22
- db: 0,
23
- driver: nil,
24
- id: nil,
25
- tcp_keepalive: 0,
26
- reconnect_attempts: 1,
27
- reconnect_delay: 0,
28
- reconnect_delay_max: 0.5,
29
- inherit_socket: false,
30
- logger: nil,
31
- sentinels: nil,
32
- role: nil
33
- }.freeze
34
-
35
- attr_reader :options
36
-
37
- def scheme
38
- @options[:scheme]
39
- end
40
-
41
- def host
42
- @options[:host]
43
- end
44
-
45
- def port
46
- @options[:port]
47
- end
48
-
49
- def path
50
- @options[:path]
51
- end
52
-
53
- def read_timeout
54
- @options[:read_timeout]
55
- end
56
-
57
- def connect_timeout
58
- @options[:connect_timeout]
59
- end
60
-
61
- def timeout
62
- @options[:read_timeout]
63
- end
64
-
65
- def username
66
- @options[:username]
67
- end
68
-
69
- def password
70
- @options[:password]
71
- end
72
-
73
- def db
74
- @options[:db]
75
- end
76
-
77
- def db=(db)
78
- @options[:db] = db.to_i
79
- end
80
-
81
- def driver
82
- @options[:driver]
83
- end
84
-
85
- def inherit_socket?
86
- @options[:inherit_socket]
87
- end
88
-
89
- attr_accessor :logger
90
- attr_reader :connection
91
- attr_reader :command_map
92
-
93
- def initialize(options = {})
94
- @options = _parse_options(options)
95
- @reconnect = true
96
- @logger = @options[:logger]
97
- @connection = nil
98
- @command_map = {}
99
-
100
- @pending_reads = 0
101
-
102
- @connector =
103
- if !@options[:sentinels].nil?
104
- Connector::Sentinel.new(@options)
105
- elsif options.include?(:connector) && options[:connector].respond_to?(:new)
106
- options.delete(:connector).new(@options)
6
+ class Client < ::RedisClient
7
+ ERROR_MAPPING = {
8
+ RedisClient::ConnectionError => Redis::ConnectionError,
9
+ RedisClient::CommandError => Redis::CommandError,
10
+ RedisClient::ReadTimeoutError => Redis::TimeoutError,
11
+ RedisClient::CannotConnectError => Redis::CannotConnectError,
12
+ RedisClient::AuthenticationError => Redis::CannotConnectError,
13
+ RedisClient::FailoverError => Redis::CannotConnectError,
14
+ RedisClient::PermissionError => Redis::PermissionError,
15
+ RedisClient::WrongTypeError => Redis::WrongTypeError,
16
+ RedisClient::ReadOnlyError => Redis::ReadOnlyError,
17
+ RedisClient::ProtocolError => Redis::ProtocolError,
18
+ RedisClient::OutOfMemoryError => Redis::OutOfMemoryError,
19
+ }
20
+
21
+ class << self
22
+ def config(**kwargs)
23
+ super(protocol: 2, **kwargs)
24
+ end
25
+
26
+ def sentinel(**kwargs)
27
+ super(protocol: 2, **kwargs, client_implementation: ::RedisClient)
28
+ end
29
+
30
+ def translate_error!(error)
31
+ redis_error = translate_error_class(error.class)
32
+ raise redis_error, error.message, error.backtrace
33
+ end
34
+
35
+ private
36
+
37
+ def translate_error_class(error_class)
38
+ ERROR_MAPPING.fetch(error_class)
39
+ rescue IndexError
40
+ if (client_error = error_class.ancestors.find { |a| ERROR_MAPPING[a] })
41
+ ERROR_MAPPING[error_class] = ERROR_MAPPING[client_error]
107
42
  else
108
- Connector.new(@options)
109
- end
110
- end
111
-
112
- def connect
113
- @pid = Process.pid
114
-
115
- # Don't try to reconnect when the connection is fresh
116
- with_reconnect(false) do
117
- establish_connection
118
- if password
119
- if username
120
- begin
121
- call [:auth, username, password]
122
- rescue CommandError # Likely on Redis < 6
123
- call [:auth, password]
124
- end
125
- else
126
- call [:auth, password]
127
- end
43
+ raise
128
44
  end
129
-
130
- call [:readonly] if @options[:readonly]
131
- call [:select, db] if db != 0
132
- call [:client, :setname, @options[:id]] if @options[:id]
133
- @connector.check(self)
134
45
  end
135
-
136
- self
137
46
  end
138
47
 
139
48
  def id
140
- @options[:id] || "redis://#{location}/#{db}"
49
+ config.id
141
50
  end
142
51
 
143
- def location
144
- path || "#{host}:#{port}"
52
+ def server_url
53
+ config.server_url
145
54
  end
146
55
 
147
- def call(command)
148
- reply = process([command]) { read }
149
- raise reply if reply.is_a?(CommandError)
150
-
151
- if block_given? && reply != 'QUEUED'
152
- yield reply
153
- else
154
- reply
155
- end
156
- end
157
-
158
- def call_loop(command, timeout = 0)
159
- error = nil
160
-
161
- result = with_socket_timeout(timeout) do
162
- process([command]) do
163
- loop do
164
- reply = read
165
- if reply.is_a?(CommandError)
166
- error = reply
167
- break
168
- else
169
- yield reply
170
- end
171
- end
172
- end
173
- end
174
-
175
- # Raise error when previous block broke out of the loop.
176
- raise error if error
177
-
178
- # Result is set to the value that the provided block used to break.
179
- result
180
- end
181
-
182
- def call_pipeline(pipeline)
183
- return [] if pipeline.futures.empty?
184
-
185
- with_reconnect pipeline.with_reconnect? do
186
- begin
187
- pipeline.finish(call_pipelined(pipeline)).tap do
188
- self.db = pipeline.db if pipeline.db
189
- end
190
- rescue ConnectionError => e
191
- return nil if pipeline.shutdown?
192
-
193
- # Assume the pipeline was sent in one piece, but execution of
194
- # SHUTDOWN caused none of the replies for commands that were executed
195
- # prior to it from coming back around.
196
- raise e
197
- end
198
- end
199
- end
200
-
201
- def call_pipelined(pipeline)
202
- return [] if pipeline.futures.empty?
203
-
204
- # The method #ensure_connected (called from #process) reconnects once on
205
- # I/O errors. To make an effort in making sure that commands are not
206
- # executed more than once, only allow reconnection before the first reply
207
- # has been read. When an error occurs after the first reply has been
208
- # read, retrying would re-execute the entire pipeline, thus re-issuing
209
- # already successfully executed commands. To circumvent this, don't retry
210
- # after the first reply has been read successfully.
211
-
212
- commands = pipeline.commands
213
-
214
- result = Array.new(commands.size)
215
- reconnect = @reconnect
216
-
217
- begin
218
- exception = nil
219
-
220
- process(commands) do
221
- pipeline.timeouts.each_with_index do |timeout, i|
222
- reply = if timeout
223
- with_socket_timeout(timeout) { read }
224
- else
225
- read
226
- end
227
- result[i] = reply
228
- @reconnect = false
229
- exception = reply if exception.nil? && reply.is_a?(CommandError)
230
- end
231
- end
232
-
233
- raise exception if exception
234
- ensure
235
- @reconnect = reconnect
236
- end
237
-
238
- result
239
- end
240
-
241
- def call_with_timeout(command, timeout, &blk)
242
- with_socket_timeout(timeout) do
243
- call(command, &blk)
244
- end
245
- rescue ConnectionError
246
- retry
247
- end
248
-
249
- def call_without_timeout(command, &blk)
250
- call_with_timeout(command, 0, &blk)
251
- end
252
-
253
- def process(commands)
254
- logging(commands) do
255
- ensure_connected do
256
- commands.each do |command|
257
- if command_map[command.first]
258
- command = command.dup
259
- command[0] = command_map[command.first]
260
- end
261
-
262
- write(command)
263
- end
264
-
265
- yield if block_given?
266
- end
267
- end
268
- end
269
-
270
- def connected?
271
- !!(connection && connection.connected?)
272
- end
273
-
274
- def disconnect
275
- connection.disconnect if connected?
56
+ def timeout
57
+ config.read_timeout
276
58
  end
277
- alias close disconnect
278
59
 
279
- def reconnect
280
- disconnect
281
- connect
60
+ def db
61
+ config.db
282
62
  end
283
63
 
284
- def io
285
- yield
286
- rescue TimeoutError => e1
287
- # Add a message to the exception without destroying the original stack
288
- e2 = TimeoutError.new("Connection timed out")
289
- e2.set_backtrace(e1.backtrace)
290
- raise e2
291
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
292
- raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
64
+ def host
65
+ config.host unless config.path
293
66
  end
294
67
 
295
- def read
296
- io do
297
- value = connection.read
298
- @pending_reads -= 1
299
- value
300
- end
68
+ def port
69
+ config.port unless config.path
301
70
  end
302
71
 
303
- def write(command)
304
- io do
305
- @pending_reads += 1
306
- connection.write(command)
307
- end
72
+ def path
73
+ config.path
308
74
  end
309
75
 
310
- def with_socket_timeout(timeout)
311
- connect unless connected?
312
- original = @options[:read_timeout]
313
-
314
- begin
315
- connection.timeout = timeout
316
- @options[:read_timeout] = timeout # for reconnection
317
- yield
318
- ensure
319
- connection.timeout = self.timeout if connected?
320
- @options[:read_timeout] = original
321
- end
76
+ def username
77
+ config.username
322
78
  end
323
79
 
324
- def without_socket_timeout(&blk)
325
- with_socket_timeout(0, &blk)
80
+ def password
81
+ config.password
326
82
  end
327
83
 
328
- def with_reconnect(val = true)
329
- original, @reconnect = @reconnect, val
330
- yield
331
- ensure
332
- @reconnect = original
333
- end
84
+ undef_method :call
85
+ undef_method :call_once
86
+ undef_method :call_once_v
87
+ undef_method :blocking_call
334
88
 
335
- def without_reconnect(&blk)
336
- with_reconnect(false, &blk)
89
+ def call_v(command, &block)
90
+ super(command, &block)
91
+ rescue ::RedisClient::Error => error
92
+ Client.translate_error!(error)
337
93
  end
338
94
 
339
- protected
340
-
341
- def logging(commands)
342
- return yield unless @logger&.debug?
343
-
344
- begin
345
- commands.each do |name, *args|
346
- logged_args = args.map do |a|
347
- if a.respond_to?(:inspect) then a.inspect
348
- elsif a.respond_to?(:to_s) then a.to_s
349
- else
350
- # handle poorly-behaved descendants of BasicObject
351
- klass = a.instance_exec { (class << self; self end).superclass }
352
- "\#<#{klass}:#{a.__id__}>"
353
- end
354
- end
355
- @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
356
- end
357
-
358
- t1 = Time.now
359
- yield
360
- ensure
361
- @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
95
+ def blocking_call_v(timeout, command, &block)
96
+ if timeout && timeout > 0
97
+ # Can't use the command timeout argument as the connection timeout
98
+ # otherwise it would be very racy. So we add the regular read_timeout on top
99
+ # to account for the network delay.
100
+ timeout += config.read_timeout
362
101
  end
363
- end
364
-
365
- def establish_connection
366
- server = @connector.resolve.dup
367
102
 
368
- @options[:host] = server[:host]
369
- @options[:port] = Integer(server[:port]) if server.include?(:port)
370
-
371
- @connection = @options[:driver].connect(@options)
372
- @pending_reads = 0
373
- rescue TimeoutError,
374
- SocketError,
375
- Errno::EADDRNOTAVAIL,
376
- Errno::ECONNREFUSED,
377
- Errno::EHOSTDOWN,
378
- Errno::EHOSTUNREACH,
379
- Errno::ENETUNREACH,
380
- Errno::ENOENT,
381
- Errno::ETIMEDOUT,
382
- Errno::EINVAL => error
383
-
384
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
103
+ super(timeout, command, &block)
104
+ rescue ::RedisClient::Error => error
105
+ Client.translate_error!(error)
385
106
  end
386
107
 
387
- def ensure_connected
388
- disconnect if @pending_reads > 0
389
-
390
- attempts = 0
391
-
392
- begin
393
- attempts += 1
394
-
395
- if connected?
396
- unless inherit_socket? || Process.pid == @pid
397
- raise InheritedError,
398
- "Tried to use a connection from a child process without reconnecting. " \
399
- "You need to reconnect to Redis after forking " \
400
- "or set :inherit_socket to true."
401
- end
402
- else
403
- connect
404
- end
405
-
406
- yield
407
- rescue BaseConnectionError
408
- disconnect
409
-
410
- if attempts <= @options[:reconnect_attempts] && @reconnect
411
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
412
- @options[:reconnect_delay_max]].min
413
-
414
- Kernel.sleep(sleep_t)
415
- retry
416
- else
417
- raise
418
- end
419
- rescue Exception
420
- disconnect
421
- raise
422
- end
108
+ def pipelined
109
+ super
110
+ rescue ::RedisClient::Error => error
111
+ Client.translate_error!(error)
423
112
  end
424
113
 
425
- def _parse_options(options)
426
- return options if options[:_parsed]
427
-
428
- defaults = DEFAULTS.dup
429
- options = options.dup
430
-
431
- defaults.keys.each do |key|
432
- # Fill in defaults if needed
433
- defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
434
-
435
- # Symbolize only keys that are needed
436
- options[key] = options[key.to_s] if options.key?(key.to_s)
437
- end
438
-
439
- url = options[:url]
440
- url = defaults[:url] if url.nil?
441
-
442
- # Override defaults from URL if given
443
- if url
444
- require "uri"
445
-
446
- uri = URI(url)
447
-
448
- if uri.scheme == "unix"
449
- defaults[:path] = uri.path
450
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
451
- defaults[:scheme] = uri.scheme
452
- defaults[:host] = uri.host if uri.host
453
- defaults[:port] = uri.port if uri.port
454
- defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
455
- defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
456
- defaults[:db] = uri.path[1..-1].to_i if uri.path
457
- defaults[:role] = :master
458
- else
459
- raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
460
- end
461
-
462
- defaults[:ssl] = true if uri.scheme == "rediss"
463
- end
464
-
465
- # Use default when option is not specified or nil
466
- defaults.keys.each do |key|
467
- options[key] = defaults[key] if options[key].nil?
468
- end
469
-
470
- if options[:path]
471
- # Unix socket
472
- options[:scheme] = "unix"
473
- options.delete(:host)
474
- options.delete(:port)
475
- else
476
- # TCP socket
477
- options[:host] = options[:host].to_s
478
- options[:port] = options[:port].to_i
479
- end
480
-
481
- if options.key?(:timeout)
482
- options[:connect_timeout] ||= options[:timeout]
483
- options[:read_timeout] ||= options[:timeout]
484
- options[:write_timeout] ||= options[:timeout]
485
- end
486
-
487
- options[:connect_timeout] = Float(options[:connect_timeout])
488
- options[:read_timeout] = Float(options[:read_timeout])
489
- options[:write_timeout] = Float(options[:write_timeout])
490
-
491
- options[:reconnect_attempts] = options[:reconnect_attempts].to_i
492
- options[:reconnect_delay] = options[:reconnect_delay].to_f
493
- options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
494
-
495
- options[:db] = options[:db].to_i
496
- options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
497
-
498
- case options[:tcp_keepalive]
499
- when Hash
500
- %i[time intvl probes].each do |key|
501
- unless options[:tcp_keepalive][key].is_a?(Integer)
502
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
503
- end
504
- end
505
-
506
- when Integer
507
- if options[:tcp_keepalive] >= 60
508
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
509
-
510
- elsif options[:tcp_keepalive] >= 30
511
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
512
-
513
- elsif options[:tcp_keepalive] >= 5
514
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
515
- end
516
- end
517
-
518
- options[:_parsed] = true
519
-
520
- options
114
+ def multi
115
+ super
116
+ rescue ::RedisClient::Error => error
117
+ Client.translate_error!(error)
521
118
  end
522
119
 
523
- def _parse_driver(driver)
524
- driver = driver.to_s if driver.is_a?(Symbol)
525
-
526
- if driver.is_a?(String)
527
- begin
528
- require_relative "connection/#{driver}"
529
- rescue LoadError, NameError
530
- begin
531
- require "redis/connection/#{driver}"
532
- rescue LoadError, NameError => error
533
- raise "Cannot load driver #{driver.inspect}: #{error.message}"
534
- end
535
- end
536
-
537
- driver = Connection.const_get(driver.capitalize)
538
- end
539
-
540
- driver
120
+ def disable_reconnection(&block)
121
+ ensure_connected(retryable: false, &block)
541
122
  end
542
123
 
543
- class Connector
544
- def initialize(options)
545
- @options = options.dup
546
- end
547
-
548
- def resolve
549
- @options
550
- end
551
-
552
- def check(client); end
553
-
554
- class Sentinel < Connector
555
- def initialize(options)
556
- super(options)
557
-
558
- @options[:db] = DEFAULTS.fetch(:db)
559
-
560
- @sentinels = @options.delete(:sentinels).dup
561
- @role = (@options[:role] || "master").to_s
562
- @master = @options[:host]
563
- end
564
-
565
- def check(client)
566
- # Check the instance is really of the role we are looking for.
567
- # We can't assume the command is supported since it was introduced
568
- # recently and this client should work with old stuff.
569
- begin
570
- role = client.call([:role])[0]
571
- rescue Redis::CommandError
572
- # Assume the test is passed if we can't get a reply from ROLE...
573
- role = @role
574
- end
575
-
576
- if role != @role
577
- client.disconnect
578
- raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
579
- end
580
- end
581
-
582
- def resolve
583
- result = case @role
584
- when "master"
585
- resolve_master
586
- when "slave"
587
- resolve_slave
588
- else
589
- raise ArgumentError, "Unknown instance role #{@role}"
590
- end
591
-
592
- result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
593
- end
594
-
595
- def sentinel_detect
596
- @sentinels.each do |sentinel|
597
- client = Client.new(@options.merge({
598
- host: sentinel[:host] || sentinel["host"],
599
- port: sentinel[:port] || sentinel["port"],
600
- username: sentinel[:username] || sentinel["username"],
601
- password: sentinel[:password] || sentinel["password"],
602
- reconnect_attempts: 0
603
- }))
604
-
605
- begin
606
- if result = yield(client)
607
- # This sentinel responded. Make sure we ask it first next time.
608
- @sentinels.delete(sentinel)
609
- @sentinels.unshift(sentinel)
610
-
611
- return result
612
- end
613
- rescue BaseConnectionError
614
- ensure
615
- client.disconnect
616
- end
617
- end
618
-
619
- raise CannotConnectError, "No sentinels available."
620
- end
621
-
622
- def resolve_master
623
- sentinel_detect do |client|
624
- if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
625
- { host: reply[0], port: reply[1] }
626
- end
627
- end
628
- end
629
-
630
- def resolve_slave
631
- sentinel_detect do |client|
632
- if reply = client.call(["sentinel", "slaves", @master])
633
- slaves = reply.map { |s| s.each_slice(2).to_h }
634
- slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
635
- slaves.reject! { |s| s.fetch('flags').include?('s_down') }
636
-
637
- if slaves.empty?
638
- raise CannotConnectError, 'No slaves available.'
639
- else
640
- slave = slaves.sample
641
- {
642
- host: slave.fetch('ip'),
643
- port: slave.fetch('port')
644
- }
645
- end
646
- end
647
- end
648
- end
649
- end
124
+ def inherit_socket!
125
+ @inherit_socket = true
650
126
  end
651
127
  end
652
128
  end