flydata 0.6.11 → 0.6.12
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.
- 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
|