flydata 0.6.11 → 0.6.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -4
- data/VERSION +1 -1
- data/flydata-core/lib/flydata-core/postgresql/source_pos.rb +34 -0
- data/flydata-core/lib/flydata-core/table_def/base.rb +10 -0
- data/flydata-core/lib/flydata-core/table_def/postgresql_table_def.rb +20 -4
- data/flydata-core/spec/postgresql/source_pos_spec.rb +43 -0
- data/flydata-core/spec/table_def/base_spec.rb +51 -0
- data/flydata.gemspec +0 -0
- data/lib/flydata/command/sender.rb +9 -6
- data/lib/flydata/command/setup.rb +6 -12
- data/lib/flydata/command/sync.rb +31 -17
- data/lib/flydata/fluent-plugins/flydata_plugin_ext/flydata_sync.rb +2 -3
- data/lib/flydata/fluent-plugins/in_mysql_binlog_flydata.rb +15 -14
- data/lib/flydata/parser/source_table.rb +4 -3
- data/lib/flydata/plugin_support/context.rb +46 -0
- data/lib/flydata/plugin_support/sync_record_emittable.rb +69 -0
- data/lib/flydata/source/component.rb +1 -1
- data/lib/flydata/source/generate_source_dump.rb +3 -2
- data/lib/flydata/source_mysql/mysql_compatibility_check.rb +12 -11
- data/lib/flydata/source_mysql/parser/dump_parser.rb +0 -4
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/alter_table_query_handler.rb +8 -2
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_position_file.rb +7 -1
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_query_dispatcher.rb +10 -4
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_query_handler.rb +8 -2
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_record_dispatcher.rb +9 -3
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_record_handler.rb +16 -34
- data/lib/flydata/source_mysql/plugin_support/context.rb +7 -0
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/ddl_query_handler.rb +11 -19
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/dml_record_handler.rb +8 -2
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/drop_database_query_handler.rb +8 -2
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/table_meta.rb +5 -1
- data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/truncate_table_query_handler.rb +8 -2
- data/lib/flydata/source_postgresql/generate_source_dump.rb +175 -0
- data/lib/flydata/source_postgresql/parse_dump_and_send.rb +126 -0
- data/lib/flydata/source_postgresql/pg_client.rb +43 -0
- data/lib/flydata/source_postgresql/postgresql_component.rb +12 -0
- data/lib/flydata/source_postgresql/setup.rb +24 -0
- data/lib/flydata/source_postgresql/source_pos.rb +18 -0
- data/lib/flydata/source_postgresql/sync_generate_table_ddl.rb +7 -15
- data/lib/flydata/sync_file_manager.rb +39 -28
- data/spec/flydata/command/setup_spec.rb +0 -1
- data/spec/flydata/command/sync_spec.rb +2 -2
- data/spec/flydata/fluent-plugins/in_mysql_binlog_flydata_spec.rb +5 -6
- data/spec/flydata/plugin_support/context_spec.rb +27 -0
- data/spec/flydata/source_mysql/parser/dump_parser_spec.rb +4 -4
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/alter_table_query_handler_spec.rb +3 -3
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_query_dispatcher_spec.rb +5 -5
- data/spec/flydata/source_mysql/plugin_support/context_spec.rb +26 -0
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/ddl_query_handler_spec.rb +3 -3
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/dml_record_handler_spec.rb +2 -2
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/drop_database_query_handler_spec.rb +3 -3
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/shared_query_handler_context.rb +3 -1
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/table_meta_spec.rb +3 -3
- data/spec/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/truncate_query_handler_spec.rb +7 -4
- data/spec/flydata/source_postgresql/generate_source_dump_spec.rb +144 -0
- data/spec/flydata/sync_file_manager_spec.rb +1 -1
- metadata +38 -24
- data/lib/flydata/fluent-plugins/mysql/context.rb +0 -25
@@ -2,15 +2,16 @@ module Flydata
|
|
2
2
|
module Parser
|
3
3
|
|
4
4
|
class SourceTable
|
5
|
-
def initialize(table_name, columns = {}
|
5
|
+
def initialize(table_name, columns = {})
|
6
6
|
@table_name = table_name
|
7
7
|
@columns = columns
|
8
8
|
@column_names = columns.collect{|k,v| v[:column_name]}
|
9
|
-
@primary_keys =
|
9
|
+
@primary_keys = [] # no longer used. keeping the instance variable for
|
10
|
+
# mashall dump compatibility
|
10
11
|
@value_converters = {}
|
11
12
|
end
|
12
13
|
|
13
|
-
attr_accessor :table_name, :
|
14
|
+
attr_accessor :table_name, :column_names, :value_converters
|
14
15
|
|
15
16
|
def add_column(column)
|
16
17
|
cn = column[:column_name]
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Flydata
|
2
|
+
module PluginSupport
|
3
|
+
class Context
|
4
|
+
|
5
|
+
def self.mandatory_opts
|
6
|
+
@mandatory_opts ||= []
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.optional_opts
|
10
|
+
@optional_opts ||= []
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.register_mandatory_opts(*opts)
|
14
|
+
@mandatory_opts ||= []
|
15
|
+
opts.each {|opt|
|
16
|
+
@mandatory_opts << opt.to_sym
|
17
|
+
attr_accessor opt
|
18
|
+
}
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.register_optional_opts(*opts)
|
23
|
+
@optional_opts ||= []
|
24
|
+
opts.each {|opt|
|
25
|
+
@optional_opts << opt.to_sym
|
26
|
+
attr_accessor opt
|
27
|
+
}
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
register_mandatory_opts :tables, :tag, :sync_fm, :omit_events, :table_revs
|
32
|
+
register_optional_opts :current_binlog_file
|
33
|
+
|
34
|
+
def initialize(opts)
|
35
|
+
missing_opts = self.class.mandatory_opts - opts.keys
|
36
|
+
unless (missing_opts.empty?)
|
37
|
+
raise "Mandatory option(s) are missing: #{missing_opts.join(', ')}"
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.each do |k, v|
|
41
|
+
self.instance_variable_set(:"@#{k}", v)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'flydata-core/record/record'
|
2
|
+
|
3
|
+
module Flydata
|
4
|
+
module PluginSupport
|
5
|
+
module SyncRecordEmittable
|
6
|
+
TABLE_NAME = :table_name # A Flydata JSON tag to specify a table name
|
7
|
+
TYPE = :type
|
8
|
+
SEQ = :seq
|
9
|
+
RESPECT_ORDER = :respect_order
|
10
|
+
SRC_POS = :src_pos
|
11
|
+
TABLE_REV = :table_rev
|
12
|
+
V = :v # FlyData record format version
|
13
|
+
|
14
|
+
attr_accessor :context # required
|
15
|
+
|
16
|
+
# Public Interface: Emit sync records to fluent engine
|
17
|
+
#
|
18
|
+
# "records" : A record or records for emitting
|
19
|
+
# Each record needs to be Hash
|
20
|
+
# "options"
|
21
|
+
# tag : (optional) tag (default: @context.tag)
|
22
|
+
# timestamp : (optional) timestamp (default: current timestamp)
|
23
|
+
# src_pos : (required) source position (used for sync:repair)
|
24
|
+
# table : (optional) table name
|
25
|
+
# increment_table_rev : (optional) set true when incrementing table revision
|
26
|
+
def emit_sync_records(records, options)
|
27
|
+
return if records.nil? # skip
|
28
|
+
records = [records] unless records.kind_of?(Array)
|
29
|
+
|
30
|
+
# Check options
|
31
|
+
tag = options[:tag] || @context.tag
|
32
|
+
timestamp = options[:timestamp] || Time.now.to_i
|
33
|
+
type = options[:type]
|
34
|
+
raise "type option must be set" if type.to_s.empty?
|
35
|
+
src_pos = options[:src_pos]
|
36
|
+
raise "src_pos option must be set" if src_pos.to_s.empty?
|
37
|
+
|
38
|
+
seq = nil
|
39
|
+
if table = options[:table]
|
40
|
+
table_rev = @context.table_revs[table]
|
41
|
+
if options[:increment_table_rev]
|
42
|
+
table_rev = @context.sync_fm.increment_table_rev(table, table_rev)
|
43
|
+
@context.table_revs[table] = table_rev
|
44
|
+
end
|
45
|
+
seq = @context.sync_fm.get_table_position(table)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Add common information to each record
|
49
|
+
array = records.collect do |r|
|
50
|
+
r[TYPE] = type
|
51
|
+
r[RESPECT_ORDER] = true
|
52
|
+
r[SRC_POS] = src_pos
|
53
|
+
r[V] = FlydataCore::Record::V2
|
54
|
+
|
55
|
+
if table
|
56
|
+
seq = @context.sync_fm.increment_table_position(seq)
|
57
|
+
r[SEQ] = seq
|
58
|
+
r[TABLE_NAME] = table
|
59
|
+
r[TABLE_REV] = table_rev
|
60
|
+
end
|
61
|
+
[timestamp, r]
|
62
|
+
end
|
63
|
+
Fluent::Engine.emit_array(tag, array)
|
64
|
+
@context.sync_fm.save_table_position(table, seq) if table
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -55,8 +55,9 @@ class GenerateSourceDump < Component
|
|
55
55
|
#
|
56
56
|
# tables: An array of tables to be dumped.
|
57
57
|
# file_path: A file path string of the dump file to which data is written.
|
58
|
-
# This value may be nil, in which case contents are written to
|
59
|
-
# pipe
|
58
|
+
# This value may be nil, in which case contents are written to
|
59
|
+
# a pipe to which the caller can access via `io` passed in the
|
60
|
+
# callback.
|
60
61
|
# src_pos_callback: A callback called when the source position of the dump
|
61
62
|
# becomes available. The callback takes the following arguments.
|
62
63
|
# io: Input IO to the dump.
|
@@ -43,17 +43,24 @@ module SourceMysql
|
|
43
43
|
stdin.close
|
44
44
|
while !stderr.eof?
|
45
45
|
lines = []
|
46
|
-
while line = stderr.gets
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
46
|
+
while line = stderr.gets do
|
47
|
+
lines << line.strip unless line =~ /Warning: Using a password on the command line interface can be insecure/
|
48
|
+
end
|
49
|
+
unless lines.empty?
|
50
|
+
err_reason = lines.join(" ")
|
51
|
+
log_error("Error occured during access to mysql server.", err: err_reason)
|
51
52
|
raise FlydataCore::MysqlCompatibilityError, "Cannot connect to MySQL database. Please make sure you can connect with this command:\n $ mysql -u #{@db_opts[:username]} -h #{@db_opts[:host]} -P #{@db_opts[:port]} #{@db_opts[:database]} --protocol=tcp -p"
|
52
53
|
end
|
53
54
|
end
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
58
|
+
def check_rds_master_status
|
59
|
+
if is_rds?
|
60
|
+
FlydataCore::Mysql::RdsMasterStatusChecker.new(@db_opts).do_check
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
57
64
|
def check_mysql_parameters_compat
|
58
65
|
begin
|
59
66
|
FlydataCore::Mysql::OptionalBinlogParameterChecker.new(@db_opts).do_check
|
@@ -63,12 +70,6 @@ module SourceMysql
|
|
63
70
|
FlydataCore::Mysql::RequiredBinlogParameterChecker.new(@db_opts).do_check
|
64
71
|
end
|
65
72
|
|
66
|
-
def check_rds_master_status
|
67
|
-
if is_rds?
|
68
|
-
FlydataCore::Mysql::RdsMasterStatusChecker.new(@db_opts).do_check
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
73
|
def check_mysql_binlog_retention
|
73
74
|
if is_rds?
|
74
75
|
run_rds_retention_check
|
@@ -326,10 +326,6 @@ EOS
|
|
326
326
|
create_table_block.call(current_table)
|
327
327
|
current_state = Flydata::Parser::State::INSERT_RECORD
|
328
328
|
check_point_block.call(current_table, dump_io.pos, bytesize, @binlog_pos, current_state)
|
329
|
-
elsif m = /^PRIMARY KEY \((?<primary_keys>[^\)]+)\)/.match(line)
|
330
|
-
current_table.primary_keys = m[:primary_keys].split(',').collect do |pk_str|
|
331
|
-
pk_str[1..-2]
|
332
|
-
end
|
333
329
|
end
|
334
330
|
end
|
335
331
|
|
data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/alter_table_query_handler.rb
RENAMED
@@ -1,7 +1,10 @@
|
|
1
1
|
require 'flydata/parser/parser_provider'
|
2
|
-
require 'flydata/
|
2
|
+
require 'flydata/source_mysql/plugin_support/ddl_query_handler'
|
3
3
|
|
4
|
-
module
|
4
|
+
module Flydata
|
5
|
+
module SourceMysql
|
6
|
+
|
7
|
+
module PluginSupport
|
5
8
|
class AlterTableQueryHandler < TableDdlQueryHandler
|
6
9
|
PATTERN = /^ALTER TABLE/i
|
7
10
|
|
@@ -39,3 +42,6 @@ EOS
|
|
39
42
|
end
|
40
43
|
end
|
41
44
|
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_query_dispatcher.rb
RENAMED
@@ -1,8 +1,11 @@
|
|
1
|
-
require 'flydata/
|
2
|
-
require 'flydata/
|
3
|
-
require 'flydata/
|
1
|
+
require 'flydata/source_mysql/plugin_support/alter_table_query_handler'
|
2
|
+
require 'flydata/source_mysql/plugin_support/truncate_table_query_handler'
|
3
|
+
require 'flydata/source_mysql/plugin_support/drop_database_query_handler'
|
4
4
|
|
5
|
-
module
|
5
|
+
module Flydata
|
6
|
+
module SourceMysql
|
7
|
+
|
8
|
+
module PluginSupport
|
6
9
|
class BinlogQueryDispatcher
|
7
10
|
def initialize
|
8
11
|
@handlers = []
|
@@ -64,3 +67,6 @@ module Mysql
|
|
64
67
|
end
|
65
68
|
end
|
66
69
|
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_query_handler.rb
RENAMED
@@ -1,6 +1,9 @@
|
|
1
|
-
require 'flydata/
|
1
|
+
require 'flydata/source_mysql/plugin_support/binlog_record_handler'
|
2
2
|
|
3
|
-
module
|
3
|
+
module Flydata
|
4
|
+
module SourceMysql
|
5
|
+
|
6
|
+
module PluginSupport
|
4
7
|
class BinlogQueryHandler < BinlogRecordHandler
|
5
8
|
# Return regexp
|
6
9
|
# This class will be used if the pattern matches with the query
|
@@ -9,3 +12,6 @@ module Mysql
|
|
9
12
|
end
|
10
13
|
end
|
11
14
|
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_record_dispatcher.rb
RENAMED
@@ -1,9 +1,12 @@
|
|
1
1
|
require 'fluent/plugin/in_mysql_binlog'
|
2
2
|
require 'binlog'
|
3
|
-
require 'flydata/
|
4
|
-
require 'flydata/
|
3
|
+
require 'flydata/source_mysql/plugin_support/dml_record_handler'
|
4
|
+
require 'flydata/source_mysql/plugin_support/binlog_query_dispatcher'
|
5
5
|
|
6
|
-
module
|
6
|
+
module Flydata
|
7
|
+
module SourceMysql
|
8
|
+
|
9
|
+
module PluginSupport
|
7
10
|
class BinlogRecordDispatcher
|
8
11
|
def dispatch(event)
|
9
12
|
method_name = "on_#{event.event_type.downcase}"
|
@@ -48,3 +51,6 @@ module Mysql
|
|
48
51
|
end
|
49
52
|
end
|
50
53
|
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/flydata/{fluent-plugins/mysql → source_mysql/plugin_support}/binlog_record_handler.rb
RENAMED
@@ -1,17 +1,14 @@
|
|
1
1
|
require 'fluent/plugin/in_mysql_binlog'
|
2
2
|
require 'binlog'
|
3
|
-
require 'flydata-core/record/record'
|
4
3
|
require 'flydata-core/mysql/binlog_pos'
|
4
|
+
require 'flydata/plugin_support/sync_record_emittable'
|
5
5
|
|
6
|
-
module
|
6
|
+
module Flydata
|
7
|
+
module SourceMysql
|
8
|
+
|
9
|
+
module PluginSupport
|
7
10
|
class BinlogRecordHandler
|
8
|
-
|
9
|
-
TYPE = :type
|
10
|
-
SEQ = :seq
|
11
|
-
RESPECT_ORDER = :respect_order
|
12
|
-
SRC_POS = :src_pos
|
13
|
-
TABLE_REV = :table_rev
|
14
|
-
V = :v # FlyData record format version
|
11
|
+
include Flydata::PluginSupport::SyncRecordEmittable
|
15
12
|
|
16
13
|
def initialize(context)
|
17
14
|
@context = context
|
@@ -29,6 +26,7 @@ module Mysql
|
|
29
26
|
end
|
30
27
|
|
31
28
|
private
|
29
|
+
|
32
30
|
def binlog_pos(record)
|
33
31
|
"#{@context.current_binlog_file}\t#{record['next_position'] - record['event_length']}"
|
34
32
|
end
|
@@ -70,34 +68,15 @@ module Mysql
|
|
70
68
|
return if records.nil? # skip
|
71
69
|
records = [records] unless records.kind_of?(Array)
|
72
70
|
|
73
|
-
table = records.first[
|
71
|
+
table = records.first[:table_name] || record['table_name']
|
74
72
|
raise "Missing table name. #{record}" if table.to_s.empty?
|
75
73
|
return unless acceptable_table?(record, table) && acceptable_event?(type, table)
|
76
74
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
table_rev = @context.sync_fm.increment_table_rev(table, table_rev)
|
83
|
-
@context.table_revs[table] = table_rev
|
84
|
-
end
|
85
|
-
r[TYPE] = type
|
86
|
-
r[RESPECT_ORDER] = true
|
87
|
-
r[TABLE_NAME] = table
|
88
|
-
r[SRC_POS] = binlog_pos(record)
|
89
|
-
r[TABLE_REV] = table_rev
|
90
|
-
r[V] = FlydataCore::Record::V2
|
91
|
-
end
|
92
|
-
|
93
|
-
# Use binlog's timestamp
|
94
|
-
timestamp = record["timestamp"].to_i
|
95
|
-
records.each do |row|
|
96
|
-
@context.sync_fm.increment_and_save_table_position(row[TABLE_NAME]) do |seq|
|
97
|
-
row[SEQ] = seq
|
98
|
-
Fluent::Engine.emit(@context.tag, timestamp, row)
|
99
|
-
end
|
100
|
-
end
|
75
|
+
emit_sync_records(records, opt.merge(
|
76
|
+
timestamp: record["timestamp"].to_i,
|
77
|
+
type: type,
|
78
|
+
table: table,
|
79
|
+
src_pos: binlog_pos(record)))
|
101
80
|
end
|
102
81
|
|
103
82
|
def check_empty_binlog
|
@@ -113,3 +92,6 @@ module Mysql
|
|
113
92
|
end
|
114
93
|
end
|
115
94
|
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
|
-
require 'flydata/
|
1
|
+
require 'flydata/source_mysql/plugin_support/binlog_query_handler'
|
2
2
|
|
3
|
-
module
|
3
|
+
module Flydata
|
4
|
+
module SourceMysql
|
5
|
+
|
6
|
+
module PluginSupport
|
4
7
|
|
5
8
|
class DdlQueryHandler < BinlogQueryHandler
|
6
9
|
DDL_TABLE_QUERY = /^(?:(?:ALTER|CREATE|DROP|RENAME) +(?:\w+ +)*TABLE +([^ ]+)|TRUNCATE +(?:TABLE +)?([^ ;]+))/i
|
@@ -39,32 +42,21 @@ end
|
|
39
42
|
class DatabaseDdlQueryHandler < DdlQueryHandler
|
40
43
|
def emit_record(type, record)
|
41
44
|
return unless acceptable_db?(record)
|
42
|
-
|
43
45
|
check_empty_binlog
|
44
46
|
|
45
47
|
opt = {}
|
46
48
|
records = yield(opt) # The block may set options as necessary
|
47
49
|
return if records.nil? # skip
|
48
50
|
records = [records] unless records.kind_of?(Array)
|
49
|
-
|
50
|
-
database = records.first[DB_NAME] || record['db_name']
|
51
|
-
|
52
51
|
return unless acceptable_event?(type)
|
53
52
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
r[SRC_POS] = binlog_pos(record)
|
59
|
-
r[V] = FlydataCore::Record::V2
|
60
|
-
end
|
61
|
-
|
62
|
-
# Use binlog's timestamp
|
63
|
-
timestamp = record["timestamp"].to_i
|
64
|
-
records.each do |row|
|
65
|
-
Fluent::Engine.emit(@context.tag, timestamp, row)
|
66
|
-
end
|
53
|
+
emit_sync_records(records, opt.merge(
|
54
|
+
timestamp: record["timestamp"].to_i,
|
55
|
+
type: type,
|
56
|
+
src_pos: binlog_pos(record)))
|
67
57
|
end
|
68
58
|
end
|
69
59
|
|
70
60
|
end
|
61
|
+
end
|
62
|
+
end
|