flydata 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ gem "activesupport", "~> 4.0.0"
7
7
  gem "json", "~> 1.8.0"
8
8
  gem "highline", "~> 1.6.19"
9
9
  gem "fluentd", "0.10.46"
10
- gem "ruby-binlog", ">= 1.0.0"
10
+ gem "ruby-binlog", ">= 1.0.1"
11
11
  gem "fluent-plugin-mysql-binlog", "~> 0.0.2"
12
12
  gem "mysql2", "~> 0.3.11"
13
13
 
data/Gemfile.lock CHANGED
@@ -94,7 +94,7 @@ GEM
94
94
  rspec-expectations (2.14.3)
95
95
  diff-lcs (>= 1.1.3, < 2.0)
96
96
  rspec-mocks (2.14.3)
97
- ruby-binlog (1.0.0)
97
+ ruby-binlog (1.0.1)
98
98
  ruby-prof (0.14.2)
99
99
  sigdump (0.2.2)
100
100
  sqlite3 (1.3.8)
@@ -122,7 +122,7 @@ DEPENDENCIES
122
122
  protected_attributes
123
123
  rest-client (~> 1.6.7)
124
124
  rspec
125
- ruby-binlog (>= 1.0.0)
125
+ ruby-binlog (>= 1.0.1)
126
126
  ruby-prof
127
127
  sqlite3
128
128
  timecop
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.6
1
+ 0.1.7
data/flydata.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = "flydata"
8
- s.version = "0.1.6"
8
+ s.version = "0.1.7"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Koichi Fujikawa"]
12
- s.date = "2014-05-22"
12
+ s.date = "2014-06-01"
13
13
  s.description = "FlyData Command Line Interface"
14
14
  s.email = "sysadmin@flydata.co"
15
15
  s.executables = ["fdmysqldump", "flydata"]
@@ -100,7 +100,7 @@ Gem::Specification.new do |s|
100
100
  s.add_runtime_dependency(%q<json>, ["~> 1.8.0"])
101
101
  s.add_runtime_dependency(%q<highline>, ["~> 1.6.19"])
102
102
  s.add_runtime_dependency(%q<fluentd>, ["= 0.10.46"])
103
- s.add_runtime_dependency(%q<ruby-binlog>, [">= 1.0.0"])
103
+ s.add_runtime_dependency(%q<ruby-binlog>, [">= 1.0.1"])
104
104
  s.add_runtime_dependency(%q<fluent-plugin-mysql-binlog>, ["~> 0.0.2"])
105
105
  s.add_runtime_dependency(%q<mysql2>, ["~> 0.3.11"])
106
106
  s.add_development_dependency(%q<bundler>, [">= 0"])
@@ -119,7 +119,7 @@ Gem::Specification.new do |s|
119
119
  s.add_dependency(%q<json>, ["~> 1.8.0"])
120
120
  s.add_dependency(%q<highline>, ["~> 1.6.19"])
121
121
  s.add_dependency(%q<fluentd>, ["= 0.10.46"])
122
- s.add_dependency(%q<ruby-binlog>, [">= 1.0.0"])
122
+ s.add_dependency(%q<ruby-binlog>, [">= 1.0.1"])
123
123
  s.add_dependency(%q<fluent-plugin-mysql-binlog>, ["~> 0.0.2"])
124
124
  s.add_dependency(%q<mysql2>, ["~> 0.3.11"])
125
125
  s.add_dependency(%q<bundler>, [">= 0"])
@@ -139,7 +139,7 @@ Gem::Specification.new do |s|
139
139
  s.add_dependency(%q<json>, ["~> 1.8.0"])
140
140
  s.add_dependency(%q<highline>, ["~> 1.6.19"])
141
141
  s.add_dependency(%q<fluentd>, ["= 0.10.46"])
142
- s.add_dependency(%q<ruby-binlog>, [">= 1.0.0"])
142
+ s.add_dependency(%q<ruby-binlog>, [">= 1.0.1"])
143
143
  s.add_dependency(%q<fluent-plugin-mysql-binlog>, ["~> 0.0.2"])
144
144
  s.add_dependency(%q<mysql2>, ["~> 0.3.11"])
145
145
  s.add_dependency(%q<bundler>, [">= 0"])
@@ -107,6 +107,7 @@ class FlydataMysqlBinlogRecordHandler < MysqlBinlogRecordHandler
107
107
  'LONGLONG' => 8
108
108
  }
109
109
  SIGNLESS_INTEGER_PREFIX = '0SL'
110
+ SRC_POS = 'src_pos'
110
111
 
111
112
  def initialize(opts)
112
113
  mandatory_opts = [:database, :tables, :tag, :sync_fm]
@@ -119,10 +120,15 @@ class FlydataMysqlBinlogRecordHandler < MysqlBinlogRecordHandler
119
120
  @tables = opts[:tables]
120
121
  @tag = opts[:tag]
121
122
  @sync_fm = opts[:sync_fm]
122
-
123
+ @current_binlog_file = ""
124
+ @first_empty_binlog = true
123
125
  @query_handler = FlydataMysqlBinlogQueryHandler.new(record_handler: self)
124
126
  end
125
127
 
128
+ def on_rotate(record)
129
+ @current_binlog_file = record["binlog_file"]
130
+ end
131
+
126
132
  def on_write_rows(record)
127
133
  emit_insert(record)
128
134
  end
@@ -167,11 +173,14 @@ class FlydataMysqlBinlogRecordHandler < MysqlBinlogRecordHandler
167
173
  return unless acceptable?(record)
168
174
 
169
175
  table = record['table_name']
170
-
176
+ position = record['next_position'] - record['event_length']
177
+ check_empty_binlog
178
+
171
179
  records = record["rows"].collect do |row|
172
180
  row = yield(row) if block_given? # Give the caller a chance to generate the correct row
173
181
  { TYPE => type, TABLE_NAME => table,
174
182
  RESPECT_ORDER => true, # Continuous sync needs record order to be kept
183
+ SRC_POS => "#{@current_binlog_file}\t#{position}",
175
184
  ROW => row.each.with_index(1).inject({}) do |h, (v, i)|
176
185
  if v.kind_of?(String)
177
186
  v = v.encode('utf-16', :undef => :replace, :invalid => :replace).encode('utf-8')
@@ -212,6 +221,18 @@ class FlydataMysqlBinlogRecordHandler < MysqlBinlogRecordHandler
212
221
  end
213
222
  end
214
223
  end
224
+
225
+ def check_empty_binlog
226
+ #Log one warning per consecutive records that have empty binlog filename
227
+ if @current_binlog_file.to_s.empty?
228
+ if @first_empty_binlog
229
+ $log.warn "Binlog file name is empty. Rotate event not received!"
230
+ @first_empty_binlog = false
231
+ end
232
+ else
233
+ @first_empty_binlog = true
234
+ end
235
+ end
215
236
  end
216
237
 
217
238
  class MysqlBinlogQueryHandler
@@ -108,6 +108,10 @@ EOT
108
108
  # la
109
109
  TEST_EVENT_INCIDENT = <<EOT
110
110
  {"marker"=>0, "timestamp"=>0, "type_code"=>26, "server_id"=>1, "event_length"=>40, "next_position"=>2883, "flags"=>32, "event_type"=>"Incident", "incident_type"=>175, "message"=>"Operation canceled"}
111
+ EOT
112
+ # - 04 ROTATE_EVENT
113
+ TEST_EVENT_ROTATE_MISSING_BINLOG_FILE = <<EOT
114
+ {"marker"=>0, "timestamp"=>0, "type_code"=>4, "server_id"=>1, "event_length"=>43, "next_position"=>0, "flags"=>32, "event_type"=>"Rotate", "binlog_pos"=>2883}
111
115
  EOT
112
116
 
113
117
  describe MysqlBinlogFlydataInput do
@@ -130,10 +134,10 @@ EOT
130
134
  plugin.event_listener(event)
131
135
  end
132
136
 
133
- def expect_emitted_records_with_rows(event, type, table, rows)
137
+ def expect_emitted_records_with_rows(event, type, table, position, binlog_file, rows)
134
138
  rows = [rows] unless rows.kind_of?(Array)
135
139
  records = rows.collect do |row|
136
- { "type"=>type, "table_name"=>table, "respect_order"=>true, "seq"=>2, "row"=>row }
140
+ { "type"=>type, "table_name"=>table, "respect_order"=>true, "seq"=>2, "src_pos"=>"#{binlog_file}\t#{position}", "row"=>row }
137
141
  end
138
142
  expect_emitted_records(event, records)
139
143
  end
@@ -152,7 +156,9 @@ EOT
152
156
  let(:query_event) { create_event(TEST_EVENT_QUERY_CREATE_DATABSE) }
153
157
  let(:table_map_event) { create_event(TEST_EVENT_TABLE_MAP) }
154
158
  let(:xid_event) { create_event(TEST_EVENT_XID) }
155
-
159
+ let(:rotate_event){ create_event(TEST_EVENT_ROTATE) }
160
+ let(:rotate_event_corrupt){ create_event(TEST_EVENT_ROTATE_MISSING_BINLOG_FILE) }
161
+
156
162
  let(:now) { Time.now }
157
163
 
158
164
  let(:table_seq_file) {
@@ -176,103 +182,164 @@ EOT
176
182
  end
177
183
 
178
184
  describe '#event_listener' do
179
- before { Test.configure_plugin(plugin, TEST_CONFIG) }
180
185
 
181
- context 'when received insert event' do
182
- it do
183
- table_seq_file.should_receive(:write).exactly(3).with(2)
184
- expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE,
185
- [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
186
+ context 'when received rotate event' do
187
+ before do
188
+ Test.configure_plugin(plugin, TEST_CONFIG)
189
+ plugin.event_listener(rotate_event)
186
190
  end
187
- end
188
191
 
189
- context 'when received insert event containing two byte UTF8 chars' do
190
- it do
191
- table_seq_file.should_receive(:write).exactly(3).with(2)
192
- expect_emitted_records_with_rows(insert_two_byte_event, :insert, TEST_TABLE,
193
- [{"1"=>"0SL00000001", "2"=>"føø"}, {"1"=>"0SL00000002", "2"=>"vår"}, {"1"=>"0SL00000003", "2"=>"høgé"}])
192
+ context 'when received insert event' do
193
+ it do
194
+ table_seq_file.should_receive(:write).exactly(3).with(2)
195
+ expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE, 628, "mysql-bin.000048",
196
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
197
+ end
194
198
  end
195
- end
196
199
 
197
- context 'when received insert event containing three byte UTF8 chars' do
198
- it do
199
- table_seq_file.should_receive(:write).exactly(3).with(2)
200
- expect_emitted_records_with_rows(insert_three_byte_event, :insert, TEST_TABLE,
201
- [{"1"=>"0SL00000001", "2"=>"富无无"}, {"1"=>"0SL00000002", "2"=>"易变的"}, {"1"=>"0SL00000003", "2"=>"切实切实"}])
200
+ context 'when received insert event containing two byte UTF8 chars' do
201
+ it do
202
+ table_seq_file.should_receive(:write).exactly(3).with(2)
203
+ expect_emitted_records_with_rows(insert_two_byte_event, :insert, TEST_TABLE, 628, "mysql-bin.000048",
204
+ [{"1"=>"0SL00000001", "2"=>"føø"}, {"1"=>"0SL00000002", "2"=>"vår"}, {"1"=>"0SL00000003", "2"=>"høgé"}])
205
+ end
202
206
  end
203
- end
204
207
 
205
- context 'when received delete event' do
206
- it do
207
- table_seq_file.should_receive(:write).twice.with(2)
208
- expect_emitted_records_with_rows(delete_event, :delete, TEST_TABLE,
209
- [{"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
208
+ context 'when received insert event containing three byte UTF8 chars' do
209
+ it do
210
+ table_seq_file.should_receive(:write).exactly(3).with(2)
211
+ expect_emitted_records_with_rows(insert_three_byte_event, :insert, TEST_TABLE, 628, "mysql-bin.000048",
212
+ [{"1"=>"0SL00000001", "2"=>"富无无"}, {"1"=>"0SL00000002", "2"=>"易变的"}, {"1"=>"0SL00000003", "2"=>"切实切实"}])
213
+ end
210
214
  end
211
- end
212
215
 
213
- context 'when received delete event containing two byte UTF8 chars' do
214
- it do
215
- table_seq_file.should_receive(:write).twice.with(2)
216
- expect_emitted_records_with_rows(delete_two_byte_event, :delete, TEST_TABLE,
217
- [{"1"=>"0SL00000002", "2"=>"vår"}, {"1"=>"0SL00000003", "2"=>"høgé"}])
216
+ context 'when received delete event' do
217
+ it do
218
+ table_seq_file.should_receive(:write).twice.with(2)
219
+ expect_emitted_records_with_rows(delete_event, :delete, TEST_TABLE, 5324, "mysql-bin.000048",
220
+ [{"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
221
+ end
218
222
  end
219
- end
220
223
 
221
- context 'when received delete event with containing byte UTF8 chars' do
222
- it do
223
- table_seq_file.should_receive(:write).twice.with(2)
224
- expect_emitted_records_with_rows(delete_three_byte_event, :delete, TEST_TABLE,
225
- [{"1"=>"0SL00000002", "2"=>"易变的"}, {"1"=>"0SL00000003", "2"=>"切实切实"}])
224
+ context 'when received delete event containing two byte UTF8 chars' do
225
+ it do
226
+ table_seq_file.should_receive(:write).twice.with(2)
227
+ expect_emitted_records_with_rows(delete_two_byte_event, :delete, TEST_TABLE, 5324, "mysql-bin.000048",
228
+ [{"1"=>"0SL00000002", "2"=>"vår"}, {"1"=>"0SL00000003", "2"=>"høgé"}])
229
+ end
226
230
  end
227
- end
228
231
 
229
- context 'when received update event' do
230
- it do
231
- table_seq_file.should_receive(:write).twice.with(2)
232
- expect_emitted_records_with_rows(update_event, :update, TEST_TABLE,
233
- [{"1"=>"0SL00000001", "2"=>"wow"}, {"1"=>"0SL00000003", "2"=>"fuga"}])
232
+ context 'when received delete event with containing byte UTF8 chars' do
233
+ it do
234
+ table_seq_file.should_receive(:write).twice.with(2)
235
+ expect_emitted_records_with_rows(delete_three_byte_event, :delete, TEST_TABLE, 5324, "mysql-bin.000048",
236
+ [{"1"=>"0SL00000002", "2"=>"易变的"}, {"1"=>"0SL00000003", "2"=>"切实切实"}])
237
+ end
234
238
  end
235
- end
236
239
 
237
- context 'when received update event with two byte utf8 chars' do
238
- it do
239
- table_seq_file.should_receive(:write).twice.with(2)
240
- expect_emitted_records_with_rows(update_two_byte_event, :update, TEST_TABLE,
241
- [{"1"=>"0SL00000001", "2"=>"∑ø∑"}, {"1"=>"0SL00000003", "2"=>"fügå"}])
240
+ context 'when received update event' do
241
+ it do
242
+ table_seq_file.should_receive(:write).twice.with(2)
243
+ expect_emitted_records_with_rows(update_event, :update, TEST_TABLE, 2528, "mysql-bin.000048",
244
+ [{"1"=>"0SL00000001", "2"=>"wow"}, {"1"=>"0SL00000003", "2"=>"fuga"}])
245
+ end
242
246
  end
243
- end
244
247
 
245
- context 'when received update event with three byte utf8 chars' do
246
- it do
247
- table_seq_file.should_receive(:write).twice.with(2)
248
- expect_emitted_records_with_rows(update_three_byte_event, :update, TEST_TABLE,
249
- [{"1"=>"0SL00000001", "2"=>"很兴奋"}, {"1"=>"0SL00000003", "2"=>"興奮虎"}])
248
+ context 'when received update event with two byte utf8 chars' do
249
+ it do
250
+ table_seq_file.should_receive(:write).twice.with(2)
251
+ expect_emitted_records_with_rows(update_two_byte_event, :update, TEST_TABLE, 2528, "mysql-bin.000048",
252
+ [{"1"=>"0SL00000001", "2"=>"∑ø∑"}, {"1"=>"0SL00000003", "2"=>"fügå"}])
253
+ end
254
+ end
255
+
256
+ context 'when received update event with three byte utf8 chars' do
257
+ it do
258
+ table_seq_file.should_receive(:write).twice.with(2)
259
+ expect_emitted_records_with_rows(update_three_byte_event, :update, TEST_TABLE, 2528, "mysql-bin.000048",
260
+ [{"1"=>"0SL00000001", "2"=>"很兴奋"}, {"1"=>"0SL00000003", "2"=>"興奮虎"}])
261
+ end
262
+ end
263
+
264
+ context 'when received event with another database name' do
265
+ it do
266
+ event = insert_event
267
+ event['db_name'] = 'another_db'
268
+ expect_no_emitted_record(event)
269
+ end
270
+ end
271
+
272
+ context 'when received event with unsupported table name' do
273
+ it do
274
+ event = insert_event
275
+ event['table_name'] = 'another_table'
276
+ expect_no_emitted_record(event)
277
+ end
250
278
  end
251
- end
252
279
 
253
- context 'when received event with another database name' do
254
- it do
255
- event = insert_event
256
- event['db_name'] = 'another_db'
257
- expect_no_emitted_record(event)
280
+ context 'when received unsupported event' do
281
+ it do
282
+ expect_no_emitted_record(query_event)
283
+ expect_no_emitted_record(table_map_event)
284
+ expect_no_emitted_record(xid_event)
285
+ end
258
286
  end
259
287
  end
288
+
289
+ context 'when rotate event is not received' do
290
+ before do
291
+ Test.configure_plugin(plugin, TEST_CONFIG)
292
+ end
260
293
 
261
- context 'when received event with unsupported table name' do
262
- it do
263
- event = insert_event
264
- event['table_name'] = 'another_table'
265
- expect_no_emitted_record(event)
294
+ it 'logs a warning and emits FET with a blank binlog file name, when it receives an insert event' do
295
+ table_seq_file.should_receive(:write).exactly(3).with(2)
296
+ expect($log).to receive(:warn).with("Binlog file name is empty. Rotate event not received!").once
297
+ expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE, 628, "",
298
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
266
299
  end
300
+
301
+ it 'logs a warning and emits FET with a blank binlog file name, when it receives an update event' do
302
+ table_seq_file.should_receive(:write).twice.with(2)
303
+ expect($log).to receive(:warn).with("Binlog file name is empty. Rotate event not received!").once
304
+ expect_emitted_records_with_rows(update_event, :update, TEST_TABLE, 2528, "",
305
+ [{"1"=>"0SL00000001", "2"=>"wow"}, {"1"=>"0SL00000003", "2"=>"fuga"}])
306
+ end
307
+
308
+ it 'logs a warning emits FET with a blank binlog file name, when it receives a delete event' do
309
+ table_seq_file.should_receive(:write).twice.with(2)
310
+ expect($log).to receive(:warn).with("Binlog file name is empty. Rotate event not received!").once
311
+ expect_emitted_records_with_rows(delete_event, :delete, TEST_TABLE, 5324, "",
312
+ [{"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
313
+ end
314
+
315
+ it 'logs warning once when it receives consecutive events' do
316
+ table_seq_file.should_receive(:write).exactly(10).with(2)
317
+ expect($log).to receive(:warn).with("Binlog file name is empty. Rotate event not received!").once
318
+ expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE, 628, "",
319
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
320
+ expect_emitted_records_with_rows(update_event, :update, TEST_TABLE, 2528, "",
321
+ [{"1"=>"0SL00000001", "2"=>"wow"}, {"1"=>"0SL00000003", "2"=>"fuga"}])
322
+ expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE, 628, "",
323
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
324
+ expect_emitted_records_with_rows(delete_event, :delete, TEST_TABLE, 5324, "",
325
+ [{"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
326
+ end
267
327
  end
268
328
 
269
- context 'when received unsupported event' do
270
- it do
271
- expect_no_emitted_record(query_event)
272
- expect_no_emitted_record(table_map_event)
273
- expect_no_emitted_record(xid_event)
329
+ context 'when received rotate event with missing binlog file' do
330
+ before do
331
+ Test.configure_plugin(plugin, TEST_CONFIG)
332
+ plugin.event_listener(rotate_event_corrupt)
274
333
  end
334
+
335
+ it 'logs a warning and emits FET with a blank binlog file name, when it receives an insert event' do
336
+ table_seq_file.should_receive(:write).exactly(3).with(2)
337
+ expect($log).to receive(:warn).with("Binlog file name is empty. Rotate event not received!").once
338
+ expect_emitted_records_with_rows(insert_event, :insert, TEST_TABLE, 628, "",
339
+ [{"1"=>"0SL00000001", "2"=>"foo"}, {"1"=>"0SL00000002", "2"=>"var"}, {"1"=>"0SL00000003", "2"=>"hoge"}])
340
+ end
275
341
  end
342
+
276
343
  end
277
344
  end
278
345
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flydata
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
4
+ version: 0.1.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-05-22 00:00:00.000000000 Z
12
+ date: 2014-06-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rest-client
@@ -114,7 +114,7 @@ dependencies:
114
114
  requirements:
115
115
  - - ! '>='
116
116
  - !ruby/object:Gem::Version
117
- version: 1.0.0
117
+ version: 1.0.1
118
118
  type: :runtime
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
@@ -122,7 +122,7 @@ dependencies:
122
122
  requirements:
123
123
  - - ! '>='
124
124
  - !ruby/object:Gem::Version
125
- version: 1.0.0
125
+ version: 1.0.1
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: fluent-plugin-mysql-binlog
128
128
  requirement: !ruby/object:Gem::Requirement
@@ -391,7 +391,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
391
391
  version: '0'
392
392
  segments:
393
393
  - 0
394
- hash: -2211529075960565235
394
+ hash: 4300353101989721136
395
395
  required_rubygems_version: !ruby/object:Gem::Requirement
396
396
  none: false
397
397
  requirements: