avro_turf 0.7.1 → 0.10.0

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.
Files changed (34) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +36 -0
  3. data/.github/workflows/ruby.yml +20 -0
  4. data/CHANGELOG.md +29 -0
  5. data/Gemfile +0 -3
  6. data/README.md +54 -16
  7. data/avro_turf.gemspec +13 -2
  8. data/lib/avro_turf.rb +14 -3
  9. data/lib/avro_turf/cached_confluent_schema_registry.rb +39 -0
  10. data/lib/avro_turf/cached_schema_registry.rb +4 -24
  11. data/lib/avro_turf/confluent_schema_registry.rb +106 -0
  12. data/lib/avro_turf/disk_cache.rb +83 -0
  13. data/lib/avro_turf/in_memory_cache.rb +38 -0
  14. data/lib/avro_turf/messaging.rb +77 -9
  15. data/lib/avro_turf/mutable_schema_store.rb +18 -0
  16. data/lib/avro_turf/schema_registry.rb +4 -77
  17. data/lib/avro_turf/schema_store.rb +36 -19
  18. data/lib/avro_turf/schema_to_avro_patch.rb +11 -0
  19. data/lib/avro_turf/test/fake_confluent_schema_registry_server.rb +141 -0
  20. data/lib/avro_turf/test/fake_schema_registry_server.rb +4 -82
  21. data/lib/avro_turf/version.rb +1 -1
  22. data/spec/cached_confluent_schema_registry_spec.rb +63 -0
  23. data/spec/confluent_schema_registry_spec.rb +9 -0
  24. data/spec/disk_cached_confluent_schema_registry_spec.rb +159 -0
  25. data/spec/messaging_spec.rb +208 -19
  26. data/spec/schema_store_spec.rb +36 -0
  27. data/spec/schema_to_avro_patch_spec.rb +42 -0
  28. data/spec/spec_helper.rb +8 -0
  29. data/spec/support/{schema_registry_context.rb → confluent_schema_registry_context.rb} +72 -8
  30. data/spec/test/fake_confluent_schema_registry_server_spec.rb +40 -0
  31. metadata +49 -16
  32. data/circle.yml +0 -4
  33. data/spec/cached_schema_registry_spec.rb +0 -41
  34. data/spec/schema_registry_spec.rb +0 -9
@@ -1,6 +1,6 @@
1
1
  require 'webmock/rspec'
2
2
  require 'avro_turf/messaging'
3
- require 'avro_turf/test/fake_schema_registry_server'
3
+ require 'avro_turf/test/fake_confluent_schema_registry_server'
4
4
 
5
5
  describe AvroTurf::Messaging do
6
6
  let(:registry_url) { "http://registry.example.com" }
@@ -15,18 +15,8 @@ describe AvroTurf::Messaging do
15
15
  }
16
16
 
17
17
  let(:message) { { "full_name" => "John Doe" } }
18
-
19
- before do
20
- FileUtils.mkdir_p("spec/schemas")
21
- end
22
-
23
- before do
24
- stub_request(:any, /^#{registry_url}/).to_rack(FakeSchemaRegistryServer)
25
- FakeSchemaRegistryServer.clear
26
- end
27
-
28
- before do
29
- define_schema "person.avsc", <<-AVSC
18
+ let(:schema_json) do
19
+ <<-AVSC
30
20
  {
31
21
  "name": "person",
32
22
  "type": "record",
@@ -39,8 +29,22 @@ describe AvroTurf::Messaging do
39
29
  }
40
30
  AVSC
41
31
  end
32
+ let(:schema) { Avro::Schema.parse(schema_json) }
42
33
 
43
- shared_examples_for "encoding and decoding" do
34
+ before do
35
+ FileUtils.mkdir_p("spec/schemas")
36
+ end
37
+
38
+ before do
39
+ stub_request(:any, /^#{registry_url}/).to_rack(FakeConfluentSchemaRegistryServer)
40
+ FakeConfluentSchemaRegistryServer.clear
41
+ end
42
+
43
+ before do
44
+ define_schema "person.avsc", schema_json
45
+ end
46
+
47
+ shared_examples_for "encoding and decoding with the schema from schema store" do
44
48
  it "encodes and decodes messages" do
45
49
  data = avro.encode(message, schema_name: "person")
46
50
  expect(avro.decode(data)).to eq message
@@ -60,10 +64,69 @@ describe AvroTurf::Messaging do
60
64
  end
61
65
  end
62
66
 
63
- it_behaves_like "encoding and decoding"
67
+ shared_examples_for 'encoding and decoding with the schema from registry' do
68
+ before do
69
+ registry = AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger)
70
+ registry.register('person', schema)
71
+ registry.register('people', schema)
72
+ end
73
+
74
+ it 'encodes and decodes messages' do
75
+ data = avro.encode(message, subject: 'person', version: 1)
76
+ expect(avro.decode(data)).to eq message
77
+ end
78
+
79
+ it "allows specifying a reader's schema by subject and version" do
80
+ data = avro.encode(message, subject: 'person', version: 1)
81
+ expect(avro.decode(data, schema_name: 'person')).to eq message
82
+ end
83
+
84
+ it 'raises AvroTurf::SchemaNotFoundError when the schema does not exist on registry' do
85
+ expect { avro.encode(message, subject: 'missing', version: 1) }.to raise_error(AvroTurf::SchemaNotFoundError)
86
+ end
87
+
88
+ it 'caches parsed schemas for decoding' do
89
+ data = avro.encode(message, subject: 'person', version: 1)
90
+ avro.decode(data)
91
+ allow(Avro::Schema).to receive(:parse).and_call_original
92
+ expect(avro.decode(data)).to eq message
93
+ expect(Avro::Schema).not_to have_received(:parse)
94
+ end
95
+ end
96
+
97
+ shared_examples_for 'encoding and decoding with the schema_id from registry' do
98
+ before do
99
+ registry = AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger)
100
+ registry.register('person', schema)
101
+ registry.register('people', schema)
102
+ end
103
+
104
+ it 'encodes and decodes messages' do
105
+ data = avro.encode(message, schema_id: 1)
106
+ expect(avro.decode(data)).to eq message
107
+ end
108
+
109
+ it 'raises AvroTurf::SchemaNotFoundError when the schema does not exist on registry' do
110
+ expect { avro.encode(message, schema_id: 5) }.to raise_error(AvroTurf::SchemaNotFoundError)
111
+ end
112
+
113
+ it 'caches parsed schemas for decoding' do
114
+ data = avro.encode(message, schema_id: 1)
115
+ avro.decode(data)
116
+ allow(Avro::Schema).to receive(:parse).and_call_original
117
+ expect(avro.decode(data)).to eq message
118
+ expect(Avro::Schema).not_to have_received(:parse)
119
+ end
120
+ end
121
+
122
+ it_behaves_like "encoding and decoding with the schema from schema store"
123
+
124
+ it_behaves_like 'encoding and decoding with the schema from registry'
125
+
126
+ it_behaves_like 'encoding and decoding with the schema_id from registry'
64
127
 
65
128
  context "with a provided registry" do
66
- let(:registry) { AvroTurf::SchemaRegistry.new(registry_url, logger: logger) }
129
+ let(:registry) { AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger) }
67
130
 
68
131
  let(:avro) do
69
132
  AvroTurf::Messaging.new(
@@ -73,13 +136,24 @@ describe AvroTurf::Messaging do
73
136
  )
74
137
  end
75
138
 
76
- it_behaves_like "encoding and decoding"
139
+ it_behaves_like "encoding and decoding with the schema from schema store"
140
+
141
+ it_behaves_like 'encoding and decoding with the schema from registry'
142
+
143
+ it_behaves_like 'encoding and decoding with the schema_id from registry'
77
144
 
78
145
  it "uses the provided registry" do
79
146
  allow(registry).to receive(:register).and_call_original
80
147
  message = { "full_name" => "John Doe" }
81
148
  avro.encode(message, schema_name: "person")
82
- expect(registry).to have_received(:register)
149
+ expect(registry).to have_received(:register).with("person", anything)
150
+ end
151
+
152
+ it "allows specifying a schema registry subject" do
153
+ allow(registry).to receive(:register).and_call_original
154
+ message = { "full_name" => "John Doe" }
155
+ avro.encode(message, schema_name: "person", subject: "people")
156
+ expect(registry).to have_received(:register).with("people", anything)
83
157
  end
84
158
  end
85
159
 
@@ -94,7 +168,7 @@ describe AvroTurf::Messaging do
94
168
  )
95
169
  end
96
170
 
97
- it_behaves_like "encoding and decoding"
171
+ it_behaves_like "encoding and decoding with the schema from schema store"
98
172
 
99
173
  it "uses the provided schema store" do
100
174
  allow(schema_store).to receive(:find).and_call_original
@@ -102,4 +176,119 @@ describe AvroTurf::Messaging do
102
176
  expect(schema_store).to have_received(:find).with("person", nil)
103
177
  end
104
178
  end
179
+
180
+ describe 'decoding with #decode_message' do
181
+ shared_examples_for "encoding and decoding with the schema from schema store" do
182
+ it "encodes and decodes messages" do
183
+ data = avro.encode(message, schema_name: "person")
184
+ result = avro.decode_message(data)
185
+ expect(result.message).to eq message
186
+ expect(result.schema_id).to eq 0
187
+ expect(result.writer_schema).to eq schema
188
+ expect(result.reader_schema).to eq nil
189
+ end
190
+
191
+ it "allows specifying a reader's schema" do
192
+ data = avro.encode(message, schema_name: "person")
193
+ result = avro.decode_message(data, schema_name: "person")
194
+ expect(result.message).to eq message
195
+ expect(result.writer_schema).to eq schema
196
+ expect(result.reader_schema).to eq schema
197
+ end
198
+
199
+ it "caches parsed schemas for decoding" do
200
+ data = avro.encode(message, schema_name: "person")
201
+ avro.decode_message(data)
202
+ allow(Avro::Schema).to receive(:parse).and_call_original
203
+ expect(avro.decode_message(data).message).to eq message
204
+ expect(Avro::Schema).not_to have_received(:parse)
205
+ end
206
+ end
207
+
208
+ shared_examples_for 'encoding and decoding with the schema from registry' do
209
+ before do
210
+ registry = AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger)
211
+ registry.register('person', schema)
212
+ registry.register('people', schema)
213
+ end
214
+
215
+ it 'encodes and decodes messages' do
216
+ data = avro.encode(message, subject: 'person', version: 1)
217
+ result = avro.decode_message(data)
218
+ expect(result.message).to eq message
219
+ expect(result.schema_id).to eq 0
220
+ end
221
+
222
+ it "allows specifying a reader's schema by subject and version" do
223
+ data = avro.encode(message, subject: 'person', version: 1)
224
+ expect(avro.decode_message(data, schema_name: 'person').message).to eq message
225
+ end
226
+
227
+ it 'raises AvroTurf::SchemaNotFoundError when the schema does not exist on registry' do
228
+ expect { avro.encode(message, subject: 'missing', version: 1) }.to raise_error(AvroTurf::SchemaNotFoundError)
229
+ end
230
+
231
+ it 'caches parsed schemas for decoding' do
232
+ data = avro.encode(message, subject: 'person', version: 1)
233
+ avro.decode_message(data)
234
+ allow(Avro::Schema).to receive(:parse).and_call_original
235
+ expect(avro.decode_message(data).message).to eq message
236
+ expect(Avro::Schema).not_to have_received(:parse)
237
+ end
238
+ end
239
+
240
+ it_behaves_like "encoding and decoding with the schema from schema store"
241
+
242
+ it_behaves_like 'encoding and decoding with the schema from registry'
243
+
244
+ context "with a provided registry" do
245
+ let(:registry) { AvroTurf::ConfluentSchemaRegistry.new(registry_url, logger: logger) }
246
+
247
+ let(:avro) do
248
+ AvroTurf::Messaging.new(
249
+ registry: registry,
250
+ schemas_path: "spec/schemas",
251
+ logger: logger
252
+ )
253
+ end
254
+
255
+ it_behaves_like "encoding and decoding with the schema from schema store"
256
+
257
+ it_behaves_like 'encoding and decoding with the schema from registry'
258
+
259
+ it "uses the provided registry" do
260
+ allow(registry).to receive(:register).and_call_original
261
+ message = { "full_name" => "John Doe" }
262
+ avro.encode(message, schema_name: "person")
263
+ expect(registry).to have_received(:register).with("person", anything)
264
+ end
265
+
266
+ it "allows specifying a schema registry subject" do
267
+ allow(registry).to receive(:register).and_call_original
268
+ message = { "full_name" => "John Doe" }
269
+ avro.encode(message, schema_name: "person", subject: "people")
270
+ expect(registry).to have_received(:register).with("people", anything)
271
+ end
272
+ end
273
+
274
+ context "with a provided schema store" do
275
+ let(:schema_store) { AvroTurf::SchemaStore.new(path: "spec/schemas") }
276
+
277
+ let(:avro) do
278
+ AvroTurf::Messaging.new(
279
+ registry_url: registry_url,
280
+ schema_store: schema_store,
281
+ logger: logger
282
+ )
283
+ end
284
+
285
+ it_behaves_like "encoding and decoding with the schema from schema store"
286
+
287
+ it "uses the provided schema store" do
288
+ allow(schema_store).to receive(:find).and_call_original
289
+ avro.encode(message, schema_name: "person")
290
+ expect(schema_store).to have_received(:find).with("person", nil)
291
+ end
292
+ end
293
+ end
105
294
  end
@@ -197,6 +197,42 @@ describe AvroTurf::SchemaStore do
197
197
  schema = store.find("person")
198
198
  expect(schema.fullname).to eq "person"
199
199
  end
200
+
201
+ it "is thread safe" do
202
+ define_schema "address.avsc", <<-AVSC
203
+ {
204
+ "type": "record",
205
+ "name": "address",
206
+ "fields": []
207
+ }
208
+ AVSC
209
+
210
+ # Set a Thread breakpoint right in the core place of race condition
211
+ expect(Avro::Name)
212
+ .to receive(:add_name)
213
+ .and_wrap_original { |m, *args|
214
+ Thread.stop
215
+ m.call(*args)
216
+ }
217
+
218
+ # Run two concurring threads which both will trigger the same schema loading
219
+ threads = 2.times.map { Thread.new { store.find("address") } }
220
+ # Wait for the moment when both threads will reach the breakpoint
221
+ sleep 0.001 until threads.all?(&:stop?)
222
+
223
+ expect {
224
+ # Resume the threads evaluation, one after one
225
+ threads.each do |thread|
226
+ next unless thread.status == 'sleep'
227
+
228
+ thread.run
229
+ sleep 0.001 until thread.stop?
230
+ end
231
+
232
+ # Ensure that threads are finished
233
+ threads.each(&:join)
234
+ }.to_not raise_error
235
+ end
200
236
  end
201
237
 
202
238
  describe "#load_schemas!" do
@@ -22,3 +22,45 @@ describe Avro::Schema do
22
22
  })
23
23
  end
24
24
  end
25
+
26
+
27
+ describe Avro::IO::DatumReader do
28
+ let(:writer_schema) do
29
+ Avro::Schema.parse <<-AVSC
30
+ {
31
+ "name": "no_default",
32
+ "type": "record",
33
+ "fields": [
34
+ { "type": "string", "name": "one" }
35
+ ]
36
+ }
37
+ AVSC
38
+ end
39
+ let(:reader_schema) do
40
+ Avro::Schema.parse <<-AVSC
41
+ {
42
+ "name": "no_default",
43
+ "type": "record",
44
+ "fields": [
45
+ { "type": "string", "name": "one" },
46
+ { "type": "string", "name": "two" }
47
+ ]
48
+ }
49
+ AVSC
50
+ end
51
+
52
+ it "raises an error for missing fields without a default" do
53
+ stream = StringIO.new
54
+ writer = Avro::IO::DatumWriter.new(writer_schema)
55
+ encoder = Avro::IO::BinaryEncoder.new(stream)
56
+ writer.write({ 'one' => 'first' }, encoder)
57
+ encoded = stream.string
58
+
59
+ stream = StringIO.new(encoded)
60
+ decoder = Avro::IO::BinaryDecoder.new(stream)
61
+ reader = Avro::IO::DatumReader.new(writer_schema, reader_schema)
62
+ expect do
63
+ reader.read(decoder)
64
+ end.to raise_error(Avro::AvroError, 'Missing data for "string" with no default')
65
+ end
66
+ end
@@ -12,6 +12,14 @@ module Helpers
12
12
  f.write(content)
13
13
  end
14
14
  end
15
+
16
+ def store_cache(path, hash)
17
+ File.write(File.join("spec/cache", path), JSON.generate(hash))
18
+ end
19
+
20
+ def load_cache(path)
21
+ JSON.parse(File.read(File.join("spec/cache", path)))
22
+ end
15
23
  end
16
24
 
17
25
  RSpec.configure do |config|
@@ -1,6 +1,6 @@
1
1
  # This shared example expects a registry variable to be defined
2
2
  # with an instance of the registry class being tested.
3
- shared_examples_for "a schema registry client" do
3
+ shared_examples_for "a confluent schema registry client" do
4
4
  let(:logger) { Logger.new(StringIO.new) }
5
5
  let(:registry_url) { "http://registry.example.com" }
6
6
  let(:subject_name) { "some-subject" }
@@ -15,8 +15,8 @@ shared_examples_for "a schema registry client" do
15
15
  end
16
16
 
17
17
  before do
18
- stub_request(:any, /^#{registry_url}/).to_rack(FakeSchemaRegistryServer)
19
- FakeSchemaRegistryServer.clear
18
+ stub_request(:any, /^#{registry_url}/).to_rack(FakeConfluentSchemaRegistryServer)
19
+ FakeConfluentSchemaRegistryServer.clear
20
20
  end
21
21
 
22
22
  describe "#register and #fetch" do
@@ -88,16 +88,18 @@ shared_examples_for "a schema registry client" do
88
88
  end
89
89
 
90
90
  describe "#subject_version" do
91
- before do
92
- 2.times do |n|
93
- registry.register(subject_name,
94
- { type: :record, name: "r#{n}", fields: [] }.to_json)
95
- end
91
+ let!(:schema_id1) do
92
+ registry.register(subject_name, { type: :record, name: "r0", fields: [] }.to_json)
93
+ end
94
+ let!(:schema_id2) do
95
+ registry.register(subject_name, { type: :record, name: "r1", fields: [] }.to_json)
96
96
  end
97
+
97
98
  let(:expected) do
98
99
  {
99
100
  name: subject_name,
100
101
  version: 1,
102
+ id: schema_id1,
101
103
  schema: { type: :record, name: "r0", fields: [] }.to_json
102
104
  }.to_json
103
105
  end
@@ -112,6 +114,7 @@ shared_examples_for "a schema registry client" do
112
114
  {
113
115
  name: subject_name,
114
116
  version: 2,
117
+ id: schema_id2,
115
118
  schema: { type: :record, name: "r1", fields: [] }.to_json
116
119
  }.to_json
117
120
  end
@@ -178,6 +181,67 @@ shared_examples_for "a schema registry client" do
178
181
  end
179
182
  end
180
183
 
184
+ describe "#global_config" do
185
+ let(:expected) do
186
+ { compatibility: 'BACKWARD' }.to_json
187
+ end
188
+
189
+ it "returns the global configuration" do
190
+ expect(registry.global_config).to eq(JSON.parse(expected))
191
+ end
192
+ end
193
+
194
+ describe "#update_global_config" do
195
+ let(:config) do
196
+ { compatibility: 'FORWARD' }
197
+ end
198
+ let(:expected) { config.to_json }
199
+
200
+ it "updates the global configuration and returns it" do
201
+ expect(registry.update_global_config(config)).to eq(JSON.parse(expected))
202
+ expect(registry.global_config).to eq(JSON.parse(expected))
203
+ end
204
+ end
205
+
206
+ describe "#subject_config" do
207
+ let(:expected) do
208
+ { compatibility: 'BACKWARD' }.to_json
209
+ end
210
+
211
+ context "when the subject config is not set" do
212
+ it "returns the global configuration" do
213
+ expect(registry.subject_config(subject_name)).to eq(JSON.parse(expected))
214
+ end
215
+ end
216
+
217
+ context "when the subject config is set" do
218
+ let(:config) do
219
+ { compatibility: 'FULL' }
220
+ end
221
+ let(:expected) { config.to_json }
222
+
223
+ before do
224
+ registry.update_subject_config(subject_name, config)
225
+ end
226
+
227
+ it "returns the subject config" do
228
+ expect(registry.subject_config(subject_name)).to eq(JSON.parse(expected))
229
+ end
230
+ end
231
+ end
232
+
233
+ describe "#update_subject_config" do
234
+ let(:config) do
235
+ { compatibility: 'NONE' }
236
+ end
237
+ let(:expected) { config.to_json }
238
+
239
+ it "updates the subject config and returns it" do
240
+ expect(registry.update_subject_config(subject_name, config)).to eq(JSON.parse(expected))
241
+ expect(registry.subject_config(subject_name)).to eq(JSON.parse(expected))
242
+ end
243
+ end
244
+
181
245
  # Monkey patch an Avro::Schema to simulate the presence of
182
246
  # active_support/core_ext.
183
247
  def break_to_json(avro_schema)