flydata 0.8.9 → 0.8.9.11
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 +5 -5
- data/.gitsha +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +38 -27
- data/Rakefile +4 -1
- data/VERSION +1 -1
- data/circle.yml +1 -1
- data/fd-build +26 -0
- data/flydata-core/lib/flydata-core/errors.rb +3 -0
- data/flydata-core/lib/flydata-core/mysql/compatibility_checker.rb +39 -1
- data/flydata-core/lib/flydata-core/table_def/redshift_table_def.rb +14 -4
- data/flydata-core/spec/mysql/compatibility_checker_spec.rb +140 -0
- data/flydata.gemspec +0 -0
- data/gemset.nix +580 -0
- data/lib/flydata/cli.rb +3 -0
- data/lib/flydata/error_reporting.rb +89 -0
- data/lib/flydata/fluent-plugins/in_mysql_binlog_flydata.rb +2 -0
- data/lib/flydata/fluent-plugins/in_postgresql_query_based_flydata.rb +5 -1
- data/lib/flydata/query_based_sync/client.rb +6 -1
- data/lib/flydata/source_mysql/mysql_compatibility_check.rb +9 -1
- data/replication.nix +30 -0
- data/shell.nix +47 -0
- data/spec/flydata/error_reporting_spec.rb +107 -0
- data/spec/flydata/source_mysql/mysql_compatibility_check_spec.rb +38 -0
- data/spec/flydata/source_mysql/parser/dump_parser_spec.rb +3 -1
- data/spec/flydata/source_mysql/sync_generate_table_ddl_spec.rb +12 -0
- metadata +122 -100
data/lib/flydata/cli.rb
CHANGED
|
@@ -3,6 +3,7 @@ require 'flydata/command_loggable'
|
|
|
3
3
|
require 'flydata/errors'
|
|
4
4
|
require 'flydata/helpers'
|
|
5
5
|
require 'flydata/source'
|
|
6
|
+
require 'flydata/error_reporting'
|
|
6
7
|
|
|
7
8
|
module Flydata
|
|
8
9
|
class Cli
|
|
@@ -18,6 +19,8 @@ module Flydata
|
|
|
18
19
|
"#{datetime} command.#{severity.to_s.downcase}: #{msg}\n"
|
|
19
20
|
end
|
|
20
21
|
end
|
|
22
|
+
|
|
23
|
+
Flydata::RollbarHookSetup.new(get_logger).setup
|
|
21
24
|
log_info("Start command.", {cmd: @args.join(' '), ver:flydata_version})
|
|
22
25
|
end
|
|
23
26
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
require 'flydata' # For FLYDATA_HOME
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
|
|
4
|
+
module Flydata
|
|
5
|
+
|
|
6
|
+
class RollbarHookSetup
|
|
7
|
+
def initialize(log,
|
|
8
|
+
access_token=ENV['ROLLBAR_ACCESS_TOKEN'],
|
|
9
|
+
environment=ENV['ROLLBAR_ENV'])
|
|
10
|
+
@log = log
|
|
11
|
+
@access_token = access_token
|
|
12
|
+
@environment = environment
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def should_setup?
|
|
16
|
+
(!already_setup?) && rollbar_installed? && rollbar_flag? && config_available?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rollbar_flag?
|
|
20
|
+
File.exists?(File.join(FLYDATA_HOME, 'use_rollbar'))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def config_available?
|
|
24
|
+
return false unless @access_token
|
|
25
|
+
return false if @access_token.empty?
|
|
26
|
+
|
|
27
|
+
return false unless @environment
|
|
28
|
+
return false if @environment.empty?
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def already_setup?
|
|
33
|
+
@log.instance_variable_defined?('@_rollbar_hook_enabled')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Use to stub in tests
|
|
37
|
+
def rollbar_gem_name
|
|
38
|
+
'rollbar'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rollbar_installed?
|
|
42
|
+
if Gem::Specification.respond_to?(:find_all_by_name)
|
|
43
|
+
Gem::Specification.find_all_by_name(rollbar_gem_name).any?
|
|
44
|
+
else
|
|
45
|
+
Gem.source_index.find_name(rollbar_gem_name).first
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def rollbar_configure
|
|
50
|
+
return unless rollbar_installed?
|
|
51
|
+
require rollbar_gem_name
|
|
52
|
+
|
|
53
|
+
Rollbar.configure do |config|
|
|
54
|
+
# Only actually send rollbar errors to endpoint in production and staging.
|
|
55
|
+
config.enabled = ['production', 'staging'].include?(@environment)
|
|
56
|
+
config.access_token = @access_token
|
|
57
|
+
config.environment = @environment || 'development'
|
|
58
|
+
|
|
59
|
+
config.custom_data_method = lambda do |message, exception, context|
|
|
60
|
+
{ flydata_home: FLYDATA_HOME }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def setup
|
|
66
|
+
return false unless should_setup?
|
|
67
|
+
|
|
68
|
+
rollbar_configure
|
|
69
|
+
|
|
70
|
+
old_error = @log.method(:error)
|
|
71
|
+
|
|
72
|
+
# Replace the existing logger's error method so that it will report to rollbar messages or exceptions
|
|
73
|
+
# when .error is called.
|
|
74
|
+
@log.define_singleton_method :error do |*args|
|
|
75
|
+
old_error.call(*args)
|
|
76
|
+
|
|
77
|
+
if $!.nil?
|
|
78
|
+
Rollbar.error(args.first) # Just the message portion
|
|
79
|
+
else
|
|
80
|
+
Rollbar.error($!) # Send the exception details
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Make this method idempotent: future calls will have no effect now.
|
|
85
|
+
@log.instance_variable_set('@_rollbar_hook_enabled', true)
|
|
86
|
+
true
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -18,6 +18,7 @@ require 'flydata/source_mysql/table_meta'
|
|
|
18
18
|
require 'flydata/source_mysql/table_ddl'
|
|
19
19
|
require 'flydata-core/fluent/config_helper'
|
|
20
20
|
require 'flydata-core/mysql/ssl'
|
|
21
|
+
require 'flydata/error_reporting'
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class MysqlBinlogFlydataInput < MysqlBinlogInput
|
|
@@ -53,6 +54,7 @@ class MysqlBinlogFlydataInput < MysqlBinlogInput
|
|
|
53
54
|
|
|
54
55
|
def configure(conf)
|
|
55
56
|
super
|
|
57
|
+
Flydata::RollbarHookSetup.new($log).setup
|
|
56
58
|
|
|
57
59
|
# SSL configuration
|
|
58
60
|
unless @ssl_ca_content.to_s.strip.empty?
|
|
@@ -7,6 +7,7 @@ require 'flydata/source_postgresql/plugin_support/context'
|
|
|
7
7
|
require 'flydata/source_postgresql/table_meta'
|
|
8
8
|
require 'flydata/source_postgresql/query_based_sync/client'
|
|
9
9
|
require 'flydata-core/postgresql/config'
|
|
10
|
+
require 'flydata/error_reporting'
|
|
10
11
|
|
|
11
12
|
module Fluent
|
|
12
13
|
|
|
@@ -20,6 +21,9 @@ class PostgresqlQueryBasedFlydataInput < Input
|
|
|
20
21
|
|
|
21
22
|
def configure(conf)
|
|
22
23
|
super
|
|
24
|
+
|
|
25
|
+
Flydata::RollbarHookSetup.new($log).setup
|
|
26
|
+
|
|
23
27
|
@dbconf = FlydataCore::Postgresql::Config.opts_for_pg(@data_entry['postgresql_data_entry_preference'])
|
|
24
28
|
$log.info "postgresql host:\"#{@host}\" port:\"#{@port}\" username:\"#{@username}\" database:\"#{@database}\" tables:\"#{@tables}\" tables_append_only:\"#{@tables_append_only}\""
|
|
25
29
|
|
|
@@ -42,7 +46,7 @@ class PostgresqlQueryBasedFlydataInput < Input
|
|
|
42
46
|
},
|
|
43
47
|
)
|
|
44
48
|
|
|
45
|
-
@client = Flydata::SourcePostgresql::QueryBasedSync::Client.new(@context)
|
|
49
|
+
@client = Flydata::SourcePostgresql::QueryBasedSync::Client.new(@context, true)
|
|
46
50
|
end
|
|
47
51
|
|
|
48
52
|
def start
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require 'thread'
|
|
2
2
|
require 'flydata-core/logger'
|
|
3
|
+
require 'flydata/error_reporting'
|
|
3
4
|
|
|
4
5
|
module Flydata
|
|
5
6
|
module QueryBasedSync
|
|
@@ -13,7 +14,7 @@ module QueryBasedSync
|
|
|
13
14
|
# params
|
|
14
15
|
# fetch_interval
|
|
15
16
|
# resource_names
|
|
16
|
-
def initialize(context)
|
|
17
|
+
def initialize(context, with_rollbar=false)
|
|
17
18
|
@context = context
|
|
18
19
|
@resource_requester = self.class::RESOURCE_REQUESTER_CLASS.new(context)
|
|
19
20
|
@response_handler = self.class::RESPONSE_HANDLER_CLASS.new(context)
|
|
@@ -22,6 +23,10 @@ module QueryBasedSync
|
|
|
22
23
|
@fetch_interval = c.nil? ? DEFAULT_FETCH_INTERVAL : c[:fetch_interval]
|
|
23
24
|
@retry_interval = c.nil? ? DEFAULT_RETRY_INTERVAL : c[:retry_interval]
|
|
24
25
|
end
|
|
26
|
+
|
|
27
|
+
if with_rollbar
|
|
28
|
+
Flydata::RollbarHookSetup.new(get_logger).setup
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
attr_reader :context
|
|
@@ -17,7 +17,13 @@ module SourceMysql
|
|
|
17
17
|
@dump_dir = options[:dump_dir] || nil
|
|
18
18
|
@backup_dir = options[:backup_dir] || nil
|
|
19
19
|
@tables = de_hash['tables']
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
begin
|
|
22
|
+
@rds = FlydataCore::Mysql::MysqlCompatibilityChecker.new(@db_opts).rds?
|
|
23
|
+
rescue FlydataCore::MissingExecutePermissionMysqlCompatibilityError => e
|
|
24
|
+
@rds = false
|
|
25
|
+
log_warn_stderr("[WARNING] #{e.message}")
|
|
26
|
+
end
|
|
21
27
|
end
|
|
22
28
|
|
|
23
29
|
def print_errors
|
|
@@ -98,6 +104,8 @@ module SourceMysql
|
|
|
98
104
|
else
|
|
99
105
|
raise e
|
|
100
106
|
end
|
|
107
|
+
rescue FlydataCore::MissingExecutePermissionMysqlCompatibilityError => e
|
|
108
|
+
log_warn_stderr("[WARNING] #{e.message}")
|
|
101
109
|
end
|
|
102
110
|
|
|
103
111
|
def is_rds?
|
data/replication.nix
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{ pkgs ? import <nixpkgs> {}
|
|
2
|
+
, fd-pkgs ? pkgs.fd-pkgs
|
|
3
|
+
}:
|
|
4
|
+
fd-pkgs.callPackage (
|
|
5
|
+
{ fd-base-dev-env
|
|
6
|
+
, stdenv
|
|
7
|
+
, cmake
|
|
8
|
+
, openssl
|
|
9
|
+
, gnustep
|
|
10
|
+
, fd-mysql
|
|
11
|
+
, fetchFromGitHub
|
|
12
|
+
, boost159
|
|
13
|
+
}:
|
|
14
|
+
|
|
15
|
+
stdenv.mkDerivation {
|
|
16
|
+
name = "mysql-replication-listener";
|
|
17
|
+
src = fetchFromGitHub {
|
|
18
|
+
owner = "flydata";
|
|
19
|
+
repo = "mysql-replication-listener";
|
|
20
|
+
rev = "80d10ad92a78833fc3d145f86152a0ba99891725";
|
|
21
|
+
sha256 = "11020wifib046yq9j0n1fjz4i6cq28md8p549w9phgjz13a74f5n";
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
buildInputs = [
|
|
25
|
+
fd-base-dev-env
|
|
26
|
+
cmake
|
|
27
|
+
openssl
|
|
28
|
+
boost159
|
|
29
|
+
];
|
|
30
|
+
}) {}
|
data/shell.nix
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{ pkgs ? import <nixpkgs> {}
|
|
2
|
+
, fd-pkgs ? pkgs.fd-pkgs
|
|
3
|
+
}:
|
|
4
|
+
|
|
5
|
+
let
|
|
6
|
+
replication = fd-pkgs.callPackage ./replication.nix {};
|
|
7
|
+
in
|
|
8
|
+
|
|
9
|
+
fd-pkgs.callPackage (
|
|
10
|
+
{ fd-base-dev-env
|
|
11
|
+
, stdenv
|
|
12
|
+
, openssl
|
|
13
|
+
, gnustep
|
|
14
|
+
, libiconv
|
|
15
|
+
, libxml2
|
|
16
|
+
, pkgconfig
|
|
17
|
+
, libxslt
|
|
18
|
+
, fd-mysql
|
|
19
|
+
, postgresql
|
|
20
|
+
, boost159
|
|
21
|
+
, sqlite
|
|
22
|
+
}:
|
|
23
|
+
|
|
24
|
+
stdenv.mkDerivation {
|
|
25
|
+
name = "flydata-agent-dev";
|
|
26
|
+
buildInputs = [
|
|
27
|
+
postgresql
|
|
28
|
+
gnustep.libobjc
|
|
29
|
+
libiconv
|
|
30
|
+
libxml2
|
|
31
|
+
pkgconfig
|
|
32
|
+
libxslt
|
|
33
|
+
openssl
|
|
34
|
+
fd-mysql.connector-c
|
|
35
|
+
replication
|
|
36
|
+
boost159
|
|
37
|
+
sqlite
|
|
38
|
+
fd-base-dev-env
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
projectDir = builtins.toString ./.;
|
|
42
|
+
|
|
43
|
+
shellHook = ''
|
|
44
|
+
! (gem list | grep bundler &>/dev/null) && gem install bundler
|
|
45
|
+
export PATH=$projectDir/bin:$PATH
|
|
46
|
+
'';
|
|
47
|
+
}) {}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require 'flydata/error_reporting'
|
|
2
|
+
require 'logger'
|
|
3
|
+
require 'rollbar'
|
|
4
|
+
|
|
5
|
+
module Flydata
|
|
6
|
+
describe RollbarHookSetup do
|
|
7
|
+
before :each do
|
|
8
|
+
allow(setup_obj).to receive(:rollbar_gem_name).and_return(rollbar_gem_name)
|
|
9
|
+
allow(File).to receive(:exists?).and_return(use_rollbar_exists)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
let(:setup_obj) {RollbarHookSetup.new(log, token, env)}
|
|
13
|
+
let(:log) {Logger.new(STDOUT)}
|
|
14
|
+
let(:token) {'sometoken'}
|
|
15
|
+
let(:env) {'test'}
|
|
16
|
+
let(:rollbar_gem_name) {'rollbar'}
|
|
17
|
+
let(:use_rollbar_exists) {true}
|
|
18
|
+
let(:log_method) {:error}
|
|
19
|
+
let(:log_message) {"my message"}
|
|
20
|
+
let(:log_args) {[log_message]}
|
|
21
|
+
|
|
22
|
+
describe 'custom data method' do
|
|
23
|
+
subject {setup_obj.setup; Rollbar.notifier.configuration.custom_data_method.call('a', Exception.new, Object.new)}
|
|
24
|
+
|
|
25
|
+
it {should include(:flydata_home)}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe 'log calls after #setup' do
|
|
29
|
+
subject {setup_obj.setup; log.send(log_method, *log_args)}
|
|
30
|
+
|
|
31
|
+
it do
|
|
32
|
+
expect(Rollbar.notifier).to receive(:configure)
|
|
33
|
+
expect(Rollbar.notifier).to receive(:log).with("error", log_message)
|
|
34
|
+
subject
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'during an exception' do
|
|
38
|
+
let (:exception) { Exception.new("oh no") }
|
|
39
|
+
subject do
|
|
40
|
+
setup_obj.setup
|
|
41
|
+
begin
|
|
42
|
+
raise exception
|
|
43
|
+
rescue Exception
|
|
44
|
+
log.send(log_method, *log_args)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it do
|
|
49
|
+
expect(Rollbar.notifier).to receive(:log).with("error", exception)
|
|
50
|
+
subject
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'no environment is specified' do
|
|
55
|
+
let(:env) { nil }
|
|
56
|
+
|
|
57
|
+
it do
|
|
58
|
+
expect(Rollbar.notifier).to_not receive(:log)
|
|
59
|
+
expect(Rollbar.notifier).to_not receive(:configure)
|
|
60
|
+
subject
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
context 'no token is specified' do
|
|
66
|
+
let(:token) { nil }
|
|
67
|
+
|
|
68
|
+
it do
|
|
69
|
+
expect(Rollbar.notifier).to_not receive(:log)
|
|
70
|
+
expect(Rollbar.notifier).to_not receive(:configure)
|
|
71
|
+
subject
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'the log method is not error' do
|
|
76
|
+
let(:log_method) { :warn }
|
|
77
|
+
|
|
78
|
+
it do
|
|
79
|
+
expect(Rollbar.notifier).to_not receive(:log)
|
|
80
|
+
expect(Rollbar.notifier).to receive(:configure)
|
|
81
|
+
subject
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
context 'the use_rollbar file does not exist' do
|
|
87
|
+
let(:use_rollbar_exists) { false }
|
|
88
|
+
|
|
89
|
+
it do
|
|
90
|
+
expect(Rollbar.notifier).to_not receive(:log)
|
|
91
|
+
expect(Rollbar.notifier).to_not receive(:configure)
|
|
92
|
+
subject
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
context 'when the rollbar gem is not installed' do
|
|
97
|
+
let(:rollbar_gem_name) {'rollbar2'} # Fake name that should not be installed
|
|
98
|
+
|
|
99
|
+
it do
|
|
100
|
+
expect(Rollbar.notifier).to_not receive(:log)
|
|
101
|
+
expect(Rollbar.notifier).to_not receive(:configure)
|
|
102
|
+
subject
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -31,6 +31,7 @@ module SourceMysql
|
|
|
31
31
|
|
|
32
32
|
let(:client) { double('client') }
|
|
33
33
|
before do
|
|
34
|
+
allow_any_instance_of(FlydataCore::Mysql::MysqlCompatibilityChecker).to receive(:rds?).and_return(false)
|
|
34
35
|
allow(Mysql2::Client).to receive(:new).and_return(client)
|
|
35
36
|
allow(client).to receive(:query).with("call mysql.rds_show_configuration;")
|
|
36
37
|
allow(client).to receive(:close)
|
|
@@ -156,6 +157,7 @@ module SourceMysql
|
|
|
156
157
|
allow(subject_object).to receive(:is_rds?).and_return(true)
|
|
157
158
|
end
|
|
158
159
|
before do
|
|
160
|
+
allow_any_instance_of(FlydataCore::Mysql::ExecutePrivilegeChecker).to receive(:do_check)
|
|
159
161
|
allow(client).to receive(:query).with("SELECT @@expire_logs_days").and_return([{"@@expire_logs_days"=>0}])
|
|
160
162
|
end
|
|
161
163
|
|
|
@@ -220,6 +222,7 @@ module SourceMysql
|
|
|
220
222
|
context 'when host is rds' do
|
|
221
223
|
before do
|
|
222
224
|
de_hash['host'] = 'rdrss.xxyyzz.rds.amazonaws.com'
|
|
225
|
+
allow_any_instance_of(FlydataCore::Mysql::MysqlCompatibilityChecker).to receive(:rds?).and_call_original
|
|
223
226
|
end
|
|
224
227
|
|
|
225
228
|
context "where backup retention period is not set" do
|
|
@@ -241,6 +244,41 @@ module SourceMysql
|
|
|
241
244
|
end
|
|
242
245
|
end
|
|
243
246
|
end
|
|
247
|
+
|
|
248
|
+
describe '#run_rds_retention_check' do
|
|
249
|
+
subject { subject_object.run_rds_retention_check }
|
|
250
|
+
|
|
251
|
+
before do
|
|
252
|
+
allow_any_instance_of(FlydataCore::Mysql::ExecutePrivilegeChecker).to receive(:do_check)
|
|
253
|
+
allow_any_instance_of(FlydataCore::Mysql::RdsRetentionChecker).to receive(:do_check)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
context 'when RdsRetentionChecker succeeds' do
|
|
257
|
+
it { expect{subject}.to_not raise_error }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
context 'when RdsRetentionChecker fails with command denied' do
|
|
261
|
+
before do
|
|
262
|
+
allow_any_instance_of(FlydataCore::Mysql::RdsRetentionChecker).to receive(:do_check).and_raise(Mysql2::Error, "execute command denied to user 'test'@'%' for routine 'mysql.rds_show_configuration'")
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it { expect{subject}.to_not raise_error }
|
|
266
|
+
|
|
267
|
+
it 'logs the error message' do
|
|
268
|
+
expect(subject_object).to receive(:log_warn_stderr)
|
|
269
|
+
|
|
270
|
+
subject
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
context 'when RdsRetentionChecker fails with unexpected error' do
|
|
275
|
+
before do
|
|
276
|
+
allow_any_instance_of(FlydataCore::Mysql::RdsRetentionChecker).to receive(:do_check).and_raise(Mysql2::Error, "error")
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
it { expect{subject}.to raise_error }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
244
282
|
end
|
|
245
283
|
|
|
246
284
|
end
|