td 0.10.99 → 0.11.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +3 -0
- data/ChangeLog +30 -1
- data/Rakefile +1 -1
- data/bin/td +21 -4
- data/contrib/completion/{td-comletion.bash → td-completion.bash} +0 -0
- data/dist/resources/exe/td +1 -1
- data/dist/resources/pkg/td +2 -2
- data/java/logging.properties +1 -0
- data/lib/td/command/common.rb +47 -18
- data/lib/td/command/export.rb +2 -2
- data/lib/td/command/import.rb +20 -71
- data/lib/td/command/job.rb +109 -16
- data/lib/td/command/list.rb +9 -7
- data/lib/td/command/query.rb +35 -27
- data/lib/td/command/result.rb +18 -2
- data/lib/td/command/runner.rb +18 -10
- data/lib/td/command/sched.rb +15 -3
- data/lib/td/command/schema.rb +5 -1
- data/lib/td/command/status.rb +1 -1
- data/lib/td/command/table.rb +26 -11
- data/lib/td/command/update.rb +9 -10
- data/lib/td/updater.rb +231 -29
- data/lib/td/version.rb +1 -3
- data/spec/td/updater_spec.rb +29 -0
- data/spec/td/version_spec.rb +1 -1
- data/td.gemspec +2 -2
- metadata +10 -40
- data/build/update-td-import-java.sh +0 -44
- data/java/VERSION +0 -1
- data/java/td-import-java.version +0 -1
- data/java/td-import.jar +0 -0
data/lib/td/command/list.rb
CHANGED
@@ -248,8 +248,8 @@ module List
|
|
248
248
|
add_list 'import:list', %w[], 'List bulk import sessions', 'import:list'
|
249
249
|
add_list 'import:show', %w[name], 'Show list of uploaded parts', 'import:show'
|
250
250
|
add_list 'import:create', %w[name db table], 'Create a new bulk import session to the the table', 'import:create logs_201201 example_db event_logs'
|
251
|
-
add_list 'import:
|
252
|
-
add_list 'import:jar_update', %w[], 'Update import jar', 'import:jar_update'
|
251
|
+
add_list 'import:jar_version', %w[], 'Show import jar version', 'import:jar_version'
|
252
|
+
add_list 'import:jar_update', %w[], 'Update import jar to the latest version', 'import:jar_update'
|
253
253
|
add_list 'import:prepare', %w[files_], 'Convert files into part file format', 'import:prepare logs/*.csv --format csv --columns time,uid,price,count --time-column "time" -o parts/'
|
254
254
|
add_list 'import:upload', %w[name files_], 'Upload or re-upload files into a bulk import session', 'import:upload parts/* --parallel 4'
|
255
255
|
add_list 'import:auto', %w[name files_], 'Upload files and automatically perform and commit the data', 'import:auto parts/* --parallel 4'
|
@@ -261,9 +261,9 @@ module List
|
|
261
261
|
add_list 'import:unfreeze', %w[name], 'Unfreeze a frozen bulk import session', 'import:unfreeze logs_201201'
|
262
262
|
|
263
263
|
add_list 'result:list', %w[], 'Show list of result URLs', 'result:list', 'results'
|
264
|
-
add_list 'result:show', %w[name], 'Describe information of a result URL', 'result
|
265
|
-
add_list 'result:create', %w[name URL], 'Create a result URL', 'result:create
|
266
|
-
add_list 'result:delete', %w[name], 'Delete a result URL', 'result:delete
|
264
|
+
add_list 'result:show', %w[name], 'Describe information of a result URL', 'result name'
|
265
|
+
add_list 'result:create', %w[name URL], 'Create a result URL', 'result:create name mysql://my-server/mydb'
|
266
|
+
add_list 'result:delete', %w[name], 'Delete a result URL', 'result:delete name'
|
267
267
|
|
268
268
|
add_list 'status', %w[], 'Show schedules, jobs, tables and results', 'status', 's'
|
269
269
|
|
@@ -346,8 +346,10 @@ module List
|
|
346
346
|
add_alias 'scheds', 'sched:list'
|
347
347
|
add_alias 'schedules', 'sched:list'
|
348
348
|
|
349
|
-
add_alias 'import',
|
350
|
-
add_alias 'imports',
|
349
|
+
add_alias 'import', 'import:show'
|
350
|
+
add_alias 'imports', 'import:list'
|
351
|
+
add_alias 'import:java_version', 'import:jar_version'
|
352
|
+
|
351
353
|
|
352
354
|
add_alias 'bulk_import', 'bulk_import:show'
|
353
355
|
add_alias 'bulk_imports', 'bulk_import:list'
|
data/lib/td/command/query.rb
CHANGED
@@ -14,7 +14,6 @@ module Command
|
|
14
14
|
priority = nil
|
15
15
|
retry_limit = nil
|
16
16
|
query = nil
|
17
|
-
sampling_all = nil
|
18
17
|
type = nil
|
19
18
|
limit = nil
|
20
19
|
exclude = false
|
@@ -38,7 +37,9 @@ module Command
|
|
38
37
|
end
|
39
38
|
format = s
|
40
39
|
}
|
41
|
-
op.on('-r', '--result RESULT_URL', 'write result to the URL (see also result:create subcommand)'
|
40
|
+
op.on('-r', '--result RESULT_URL', 'write result to the URL (see also result:create subcommand)',
|
41
|
+
' It is suggested for this option to be used with the -x / --exclude option to suppress printing',
|
42
|
+
' of the query result to stdout or -o / --output to dump the query result into a file.') {|s|
|
42
43
|
result_url = s
|
43
44
|
}
|
44
45
|
op.on('-u', '--user NAME', 'set user name for the result URL') {|s|
|
@@ -62,8 +63,10 @@ module Command
|
|
62
63
|
op.on('-T', '--type TYPE', 'set query type (hive, pig, impala, presto)') {|s|
|
63
64
|
type = s.to_sym
|
64
65
|
}
|
65
|
-
op.on('--sampling DENOMINATOR', 'enable random sampling to reduce records 1/DENOMINATOR', Integer) {|i|
|
66
|
-
|
66
|
+
op.on('--sampling DENOMINATOR', 'OBSOLETE - enable random sampling to reduce records 1/DENOMINATOR', Integer) {|i|
|
67
|
+
puts "WARNING: the random sampling feature enabled through the '--sampling' option was removed and does no longer"
|
68
|
+
puts " have any effect. It is left for backwards compatibility with older scripts using 'td'."
|
69
|
+
puts
|
67
70
|
}
|
68
71
|
op.on('-l', '--limit ROWS', 'limit the number of result rows shown when not outputting to file') {|s|
|
69
72
|
unless s.to_i > 0
|
@@ -80,23 +83,11 @@ module Command
|
|
80
83
|
|
81
84
|
sql = op.cmd_parse
|
82
85
|
|
83
|
-
#
|
84
|
-
|
85
|
-
if output.nil? && format
|
86
|
-
unless ['tsv', 'csv', 'json'].include?(format)
|
87
|
-
raise "Supported formats are only tsv, csv and json without --output option"
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
if render_opts[:header]
|
92
|
-
unless ['tsv', 'csv'].include?(format)
|
93
|
-
raise "Option -c / --column-header is only supported with tsv and csv formats"
|
94
|
-
end
|
95
|
-
end
|
86
|
+
# required parameters
|
96
87
|
|
97
88
|
unless db_name
|
98
|
-
|
99
|
-
|
89
|
+
raise ParameterConfigurationError,
|
90
|
+
"-d / --database DB_NAME option is required."
|
100
91
|
end
|
101
92
|
|
102
93
|
if sql == '-'
|
@@ -104,30 +95,47 @@ module Command
|
|
104
95
|
elsif sql.nil?
|
105
96
|
sql = query
|
106
97
|
end
|
107
|
-
|
108
98
|
unless sql
|
109
|
-
|
110
|
-
|
99
|
+
raise ParameterConfigurationError,
|
100
|
+
"<sql> argument or -q / --query PATH option is required."
|
101
|
+
end
|
102
|
+
|
103
|
+
# parameter concurrency validation
|
104
|
+
|
105
|
+
if output.nil? && format
|
106
|
+
unless ['tsv', 'csv', 'json'].include?(format)
|
107
|
+
raise ParameterConfigurationError,
|
108
|
+
"Supported formats are only tsv, csv and json without --output option"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
if render_opts[:header]
|
113
|
+
unless ['tsv', 'csv'].include?(format)
|
114
|
+
raise ParameterConfigurationError,
|
115
|
+
"Option -c / --column-header is only supported with tsv and csv formats"
|
116
|
+
end
|
111
117
|
end
|
112
118
|
|
113
119
|
if result_url
|
114
120
|
require 'td/command/result'
|
115
121
|
result_url = build_result_url(result_url, result_user, result_ask_password)
|
122
|
+
if result_url =~ /^td:/
|
123
|
+
validate_td_result_url(result_url)
|
124
|
+
end
|
116
125
|
end
|
117
126
|
|
118
127
|
client = get_client
|
119
128
|
|
120
|
-
# local
|
129
|
+
# local existence check
|
121
130
|
get_database(client, db_name)
|
122
131
|
|
123
132
|
opts = {}
|
124
|
-
opts['sampling_all'] = sampling_all if sampling_all
|
125
133
|
opts['type'] = type if type
|
126
134
|
job = client.query(db_name, sql, result_url, priority, retry_limit, opts)
|
127
135
|
|
128
|
-
|
129
|
-
|
130
|
-
|
136
|
+
puts "Job #{job.job_id} is queued."
|
137
|
+
puts "Use '#{$prog} " + Config.cl_apikey_string + "job:show #{job.job_id}' to show the status."
|
138
|
+
#puts "See #{job.url} to see the progress."
|
131
139
|
|
132
140
|
if wait
|
133
141
|
wait_job(job, true)
|
data/lib/td/command/result.rb
CHANGED
@@ -56,8 +56,7 @@ module Command
|
|
56
56
|
}
|
57
57
|
|
58
58
|
name, url = op.cmd_parse
|
59
|
-
|
60
|
-
API.validate_database_name(name)
|
59
|
+
API.validate_result_set_name(name)
|
61
60
|
|
62
61
|
client = get_client
|
63
62
|
|
@@ -123,6 +122,23 @@ module Command
|
|
123
122
|
|
124
123
|
url
|
125
124
|
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def validate_td_result_url(url)
|
128
|
+
re = /td:\/\/[^@]*@\/(.*)\/(.*)?/
|
129
|
+
match = re.match(url)
|
130
|
+
if match.nil?
|
131
|
+
raise ParameterConfigurationError, "Treasure Data result output invalid URL format"
|
132
|
+
end
|
133
|
+
dbs = match[1]
|
134
|
+
tbl = match[2]
|
135
|
+
begin
|
136
|
+
API.validate_name("Treasure Data result output destination database", 3, 256, dbs)
|
137
|
+
API.validate_name("Treasure Data result output destination table", 3, 256, tbl)
|
138
|
+
rescue ParameterValidationError => e
|
139
|
+
raise ParameterConfigurationError, e
|
140
|
+
end
|
141
|
+
end
|
126
142
|
end
|
127
143
|
end
|
128
144
|
|
data/lib/td/command/runner.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
module TreasureData
|
3
3
|
module Command
|
4
4
|
|
5
|
-
|
6
5
|
class Runner
|
7
6
|
def initialize
|
8
7
|
@config_path = nil
|
@@ -21,7 +20,7 @@ class Runner
|
|
21
20
|
$prog = @prog_name || File.basename($0)
|
22
21
|
|
23
22
|
op = OptionParser.new
|
24
|
-
op.version =
|
23
|
+
op.version = TOOLBELT_VERSION
|
25
24
|
op.banner = <<EOF
|
26
25
|
usage: #{$prog} [options] COMMAND [args]
|
27
26
|
|
@@ -45,11 +44,11 @@ Basic commands:
|
|
45
44
|
import # manage bulk import sessions (Java based fast processing)
|
46
45
|
bulk_import # manage bulk import sessions (Old Ruby-based implementation)
|
47
46
|
result # create/delete/list result URLs
|
47
|
+
sched # create/delete/list schedules that run a query periodically
|
48
|
+
schema # create/delete/modify schemas of tables
|
48
49
|
|
49
50
|
Additional commands:
|
50
51
|
|
51
|
-
sched # create/delete/list schedules that run a query periodically
|
52
|
-
schema # create/delete/modify schemas of tables
|
53
52
|
status # show scheds, jobs, tables and results
|
54
53
|
apikey # show/set API key
|
55
54
|
server # show status of the Treasure Data server
|
@@ -141,12 +140,19 @@ EOF
|
|
141
140
|
$stderr.puts "TreasureData account is not configured yet."
|
142
141
|
$stderr.puts "Run '#{$prog} account' first."
|
143
142
|
rescue => e
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
143
|
+
# work in progress look ahead development: new exceptions are rendered as simple
|
144
|
+
# error messages unless the TD_TOOLBELT_DEBUG variable is not empty.
|
145
|
+
# List of new exceptions:
|
146
|
+
# => ParameterConfigurationError
|
147
|
+
# => BulkImportExecutionError
|
148
|
+
unless [ParameterConfigurationError, BulkImportExecutionError].include?(e.class) && ENV['TD_TOOLBELT_DEBUG'].nil?
|
149
|
+
$stderr.puts "error #{$!.class}: backtrace:"
|
150
|
+
$!.backtrace.each {|b|
|
151
|
+
$stderr.puts " #{b}"
|
152
|
+
}
|
153
|
+
puts ""
|
154
|
+
end
|
155
|
+
puts "Error: " + $!.to_s
|
150
156
|
|
151
157
|
require 'socket'
|
152
158
|
if e.is_a?(::SocketError)
|
@@ -157,7 +163,9 @@ If you want to use td command through a proxy,
|
|
157
163
|
please set HTTP_PROXY environment variable (e.g. export HTTP_PROXY="host:port")
|
158
164
|
EOS
|
159
165
|
end
|
166
|
+
return 1
|
160
167
|
end
|
168
|
+
return 0
|
161
169
|
end
|
162
170
|
end
|
163
171
|
|
data/lib/td/command/sched.rb
CHANGED
@@ -41,7 +41,13 @@ module Command
|
|
41
41
|
op.on('-d', '--database DB_NAME', 'use the database (required)') {|s|
|
42
42
|
db_name = s
|
43
43
|
}
|
44
|
-
op.on('-t', '--timezone TZ',
|
44
|
+
op.on('-t', '--timezone TZ', "name of the timezone.",
|
45
|
+
" Only extended timezones like 'Asia/Tokyo', 'America/Los_Angeles' are supported,",
|
46
|
+
" (no 'PST', 'PDT', etc...).",
|
47
|
+
" When a timezone is specified, the cron schedule is referred to that timezone.",
|
48
|
+
" Otherwise, the cron schedule is referred to the UTC timezone.",
|
49
|
+
" E.g. cron schedule '0 12 * * *' will execute daily at 5 AM without timezone option",
|
50
|
+
" and at 12PM with the -t / --timezone 'America/Los_Angeles' timezone option") {|s|
|
45
51
|
timezone = s
|
46
52
|
}
|
47
53
|
op.on('-D', '--delay SECONDS', 'delay time of the schedule', Integer) {|i|
|
@@ -97,7 +103,7 @@ module Command
|
|
97
103
|
|
98
104
|
client = get_client
|
99
105
|
|
100
|
-
# local
|
106
|
+
# local existence check
|
101
107
|
get_database(client, db_name)
|
102
108
|
|
103
109
|
begin
|
@@ -153,7 +159,13 @@ module Command
|
|
153
159
|
op.on('-r', '--result RESULT_TABLE', 'change the result table') {|s|
|
154
160
|
result = s
|
155
161
|
}
|
156
|
-
op.on('-t', '--timezone TZ',
|
162
|
+
op.on('-t', '--timezone TZ', "name of the timezone.",
|
163
|
+
" Only extended timezones like 'Asia/Tokyo', 'America/Los_Angeles' are supported,",
|
164
|
+
" (no 'PST', 'PDT', etc...).",
|
165
|
+
" When a timezone is specified, the cron schedule is referred to that timezone.",
|
166
|
+
" Otherwise, the cron schedule is referred to the UTC timezone.",
|
167
|
+
" E.g. cron schedule '0 12 * * *' will execute daily at 5 AM without timezone option",
|
168
|
+
" and at 12PM with the -t / --timezone 'America/Los_Angeles' timezone option") {|s|
|
157
169
|
timezone = s
|
158
170
|
}
|
159
171
|
op.on('-D', '--delay SECONDS', 'change the delay time of the schedule', Integer) {|i|
|
data/lib/td/command/schema.rb
CHANGED
@@ -74,7 +74,11 @@ module Command
|
|
74
74
|
name = name.to_s
|
75
75
|
type = type.to_s
|
76
76
|
|
77
|
-
|
77
|
+
begin
|
78
|
+
API.validate_column_name(name)
|
79
|
+
rescue ParameterValidationError => e
|
80
|
+
raise ParameterConfigurationError, e
|
81
|
+
end
|
78
82
|
#type = API.normalize_type_name(type)
|
79
83
|
|
80
84
|
if schema.fields.find {|f| f.name == name }
|
data/lib/td/command/status.rb
CHANGED
@@ -34,7 +34,7 @@ module Command
|
|
34
34
|
j = client.jobs(0, 4)
|
35
35
|
j.each {|job|
|
36
36
|
start = job.start_at
|
37
|
-
elapsed =
|
37
|
+
elapsed = humanize_elapsed_time(start, job.end_at)
|
38
38
|
jobs << {:JobID => job.job_id, :Status => job.status, :Query => job.query.to_s, :Start => (start ? start.localtime : ''), :Elapsed => elapsed, :Result => job.result_url}
|
39
39
|
}
|
40
40
|
x2, y2 = status_render(0, 0, "[Jobs]", jobs, :fields => [:JobID, :Status, :Start, :Elapsed, :Result, :Query])
|
data/lib/td/command/table.rb
CHANGED
@@ -144,20 +144,27 @@ module Command
|
|
144
144
|
databases = client.databases
|
145
145
|
end
|
146
146
|
|
147
|
+
has_item = databases.select {|db| db.tables.select {|table| table.type == :item}.length > 0 }.length > 0
|
148
|
+
|
147
149
|
rows = []
|
148
150
|
::Parallel.each(databases, :in_threads => num_threads) {|db|
|
149
151
|
begin
|
152
|
+
db.tables.each {}
|
150
153
|
db.tables.each {|table|
|
151
154
|
pschema = table.schema.fields.map {|f|
|
152
155
|
"#{f.name}:#{f.type}"
|
153
156
|
}.join(', ')
|
154
|
-
|
157
|
+
new_row = {
|
155
158
|
:Database => db.name, :Table => table.name, :Type => table.type.to_s, :Count => TreasureData::Helpers.format_with_delimiter(table.count),
|
156
159
|
:Size => show_size_in_bytes ? TreasureData::Helpers.format_with_delimiter(table.estimated_storage_size) : table.estimated_storage_size_string,
|
157
160
|
'Last import' => table.last_import ? table.last_import.localtime : nil,
|
158
161
|
'Last log timestamp' => table.last_log_timestamp ? table.last_log_timestamp.localtime : nil,
|
159
162
|
:Schema => pschema
|
160
163
|
}
|
164
|
+
if has_item and table.type == :item
|
165
|
+
new_row['Primary key'] = "#{table.primary_key}:#{table.primary_key_type}"
|
166
|
+
end
|
167
|
+
rows << new_row
|
161
168
|
}
|
162
169
|
rescue APIError => e
|
163
170
|
# ignores permission error because db:list shows all databases
|
@@ -171,7 +178,13 @@ module Command
|
|
171
178
|
[map[:Database], map[:Type].size, map[:Table]]
|
172
179
|
}
|
173
180
|
|
174
|
-
|
181
|
+
fields = []
|
182
|
+
if has_item
|
183
|
+
fields = [:Database, :Table, :Type, :Count, :Size, 'Last import', 'Last log timestamp', 'Primary key', :Schema]
|
184
|
+
else
|
185
|
+
fields = [:Database, :Table, :Type, :Count, :Size, 'Last import', 'Last log timestamp', :Schema]
|
186
|
+
end
|
187
|
+
puts cmd_render_table(rows, :fields => fields, :max_width => 500, :render_format => op.render_format)
|
175
188
|
|
176
189
|
if rows.empty?
|
177
190
|
if db_name
|
@@ -207,10 +220,12 @@ module Command
|
|
207
220
|
|
208
221
|
table = get_table(client, db_name, table_name)
|
209
222
|
|
210
|
-
puts "Name
|
211
|
-
puts "Type
|
212
|
-
puts "Count
|
213
|
-
puts
|
223
|
+
puts "Name : #{table.db_name}.#{table.name}"
|
224
|
+
puts "Type : #{table.type}"
|
225
|
+
puts "Count : #{table.count}"
|
226
|
+
# p table.methods.each {|m| puts m}
|
227
|
+
puts "Primary key : #{table.primary_key}:#{table.primary_key_type}" if table.type == :item
|
228
|
+
puts "Schema : ("
|
214
229
|
table.schema.fields.each {|f|
|
215
230
|
puts " #{f.name}:#{f.type}"
|
216
231
|
}
|
@@ -334,7 +349,7 @@ module Command
|
|
334
349
|
to = nil
|
335
350
|
wait = false
|
336
351
|
|
337
|
-
op.on('-t', '--to TIME', 'end time of logs to delete') {|s|
|
352
|
+
op.on('-t', '--to TIME', 'end time of logs to delete in Unix time multiple of 3600 (1 hour)') {|s|
|
338
353
|
if s.to_i.to_s == s
|
339
354
|
# UNIX time
|
340
355
|
to = s.to_i
|
@@ -343,7 +358,7 @@ module Command
|
|
343
358
|
to = Time.parse(s).to_i
|
344
359
|
end
|
345
360
|
}
|
346
|
-
op.on('-f', '--from TIME', 'start time of logs to delete') {|s|
|
361
|
+
op.on('-f', '--from TIME', 'start time of logs to delete in Unix time multiple of 3600 (1 hour)') {|s|
|
347
362
|
if s.to_i.to_s == s
|
348
363
|
from = s.to_i
|
349
364
|
else
|
@@ -351,7 +366,7 @@ module Command
|
|
351
366
|
from = Time.parse(s).to_i
|
352
367
|
end
|
353
368
|
}
|
354
|
-
op.on('-w', '--wait', 'wait for
|
369
|
+
op.on('-w', '--wait', 'wait for the job to finish', TrueClass) {|b|
|
355
370
|
wait = b
|
356
371
|
}
|
357
372
|
|
@@ -368,7 +383,7 @@ module Command
|
|
368
383
|
end
|
369
384
|
|
370
385
|
if from % 3600 != 0 || to % 3600 != 0
|
371
|
-
$stderr.puts "
|
386
|
+
$stderr.puts "Time for the -f / --from and -t / --to options must be a multiple of 3600 (1 hour)"
|
372
387
|
exit 1
|
373
388
|
end
|
374
389
|
|
@@ -402,7 +417,7 @@ module Command
|
|
402
417
|
|
403
418
|
$stderr.puts "Table set to expire data older than #{expire_days} days."
|
404
419
|
end
|
405
|
-
|
420
|
+
|
406
421
|
|
407
422
|
IMPORT_TEMPLATES = {
|
408
423
|
'apache' => [
|
data/lib/td/command/update.rb
CHANGED
@@ -4,21 +4,20 @@ module TreasureData
|
|
4
4
|
module Command
|
5
5
|
|
6
6
|
def update(op)
|
7
|
-
|
8
|
-
|
7
|
+
# for gem installation, this command is disallowed -
|
8
|
+
# it only works for the toolbelt.
|
9
|
+
if Updater.disable?
|
10
|
+
$stderr.puts Updater.disable_message
|
9
11
|
exit
|
10
12
|
end
|
11
13
|
|
12
|
-
|
13
|
-
Updating
|
14
|
-
|
15
|
-
|
16
|
-
if new_version = TreasureData::Updater.update
|
17
|
-
$stderr.puts "updated to #{new_version}"
|
14
|
+
start_time = Time.now
|
15
|
+
puts "Updating 'td' from #{TOOLBELT_VERSION}..."
|
16
|
+
if new_version = Updater.update
|
17
|
+
puts "Successfully updated to #{new_version} in #{humanize_time((Time.now - start_time).to_i)}."
|
18
18
|
else
|
19
|
-
|
19
|
+
puts "Nothing to update."
|
20
20
|
end
|
21
|
-
$stderr.puts "ended at #{Time.now}"
|
22
21
|
end
|
23
22
|
|
24
23
|
end
|