schema_registry_client 0.0.11.pre.beta1 → 0.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7aa1f66862cabf744233ac1fa0dcc199b7fbaaabb448d50f8e8b3aa0fb98f273
4
- data.tar.gz: 642bfb50cd4efa20bdc93fb2740ed0ec6788866b378c7d601d5e7b05e8e6a9cf
3
+ metadata.gz: '09a16bc0d516a981c823ec6aef2c356cae10f4dca61279870b741741c1f9baa4'
4
+ data.tar.gz: 7bbcbe0b5b6ffeb494c264983e137fc1864387440bc9ecc41b21f2405d15a361
5
5
  SHA512:
6
- metadata.gz: 58254805ac900a92e2b7e6418b1e1565dd2d23b257ce6c771d26a1e1dd753193de6836e417c5382c083d322ded4f0a67947898057e8656cf053f1d3b713d31b2
7
- data.tar.gz: db29da6cc31f095f9f83d6c26b8b2de06a52d889d2ef1a445ba6861529f34014841dda8068cc26c2945dc016a08f884aebdd7c701ad0650f2feaf9bcb854d67b
6
+ metadata.gz: 88a82ff70e5fdc2a5b5b783630a218a58b02cdc1f2513470ffd31f14e1d75356eff1f1ccef7e684954ce9d72ce07571e71b50900e61e1cf98de998d3513a5718
7
+ data.tar.gz: 9c5a98b3299615b052a8eb2f9f4356cc75dea85b283c5cee8e8759b97c373be17350a7df9d3f9232200cf3d6f406dd64ddff80d4292d5be3982807779f9ae4b9
data/.gitignore CHANGED
@@ -1 +1,2 @@
1
1
  coverage
2
+ .claude
data/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## UNRELEASED
9
9
 
10
+ # 0.0.11 - 2026-02-20
11
+
12
+ * Fixes for caching working correctly and ensuring we don't recalculate / re-parse Avro schemas
13
+
10
14
  # 0.0.10 - 2026-02-11
11
15
 
12
16
  * Fix: Do not send `schemaType` or `references` if schema type is `AVRO`, for backwards compatibility with older schema registries.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- schema_registry_client (0.0.11.pre.beta1)
4
+ schema_registry_client (0.0.11)
5
5
  avro
6
6
  excon
7
7
  google-protobuf
@@ -8,6 +8,7 @@ module SchemaRegistry
8
8
  @schemas_by_id = {}
9
9
  @ids_by_schema = {}
10
10
  @versions_by_subject_and_id = {}
11
+ @mutex = Mutex.new
11
12
  end
12
13
 
13
14
  # Delegate the following methods to the upstream
@@ -20,25 +21,37 @@ module SchemaRegistry
20
21
  # @param id [Integer] the schema ID to fetch
21
22
  # @return [String] the schema string stored in the registry for the given id
22
23
  def fetch(id)
23
- @schemas_by_id[id] ||= @upstream.fetch(id)
24
+ @mutex.synchronize do
25
+ @schemas_by_id[id] ||= @upstream.fetch(id)
26
+ end
24
27
  end
25
28
 
26
29
  # @param id [Integer] the schema ID to fetch
27
30
  # @param subject [String] the subject to fetch the version for
28
31
  # @return [Integer, nil] the version of the schema for the given subject and id, or nil if not found
29
32
  def fetch_version(id, subject)
30
- key = [subject, id]
31
- return @versions_by_subject_and_id[key] if @versions_by_subject_and_id[key]
33
+ @mutex.synchronize do
34
+ key = [subject, id]
35
+ return @versions_by_subject_and_id[key] if @versions_by_subject_and_id[key]
32
36
 
33
- results = @upstream.schema_subject_versions(id)
34
- @versions_by_subject_and_id[key] = results&.find { |r| r["subject"] == subject }&.dig("version")
37
+ results = @upstream.schema_subject_versions(id)
38
+ @versions_by_subject_and_id[key] = results&.find { |r| r["subject"] == subject }&.dig("version")
39
+ end
35
40
  end
36
41
 
37
42
  # @param subject [String] the subject to check
38
43
  # @param schema [String] the schema text to check
39
44
  # @return [Boolean] true if we know the schema has been registered for that subject.
40
45
  def registered?(subject, schema)
41
- @ids_by_schema[[subject, schema]] && !@ids_by_schema[[subject, schema]].empty?
46
+ @mutex.synchronize do
47
+ @ids_by_schema.key?([subject, schema])
48
+ end
49
+ end
50
+
51
+ def fetch_id(subject, schema)
52
+ @mutex.synchronize do
53
+ @ids_by_schema[[subject, schema]]
54
+ end
42
55
  end
43
56
 
44
57
  # @param subject [String] the subject to register the schema under
@@ -46,12 +59,14 @@ module SchemaRegistry
46
59
  # @param references [Array<Hash>] optional references to other schemas
47
60
  # @param schema_type [String]
48
61
  def register(subject, schema, references: [], schema_type: "PROTOBUF")
49
- key = [subject, schema]
62
+ @mutex.synchronize do
63
+ key = [subject, schema]
50
64
 
51
- @ids_by_schema[key] ||= @upstream.register(subject,
52
- schema,
53
- references: references,
54
- schema_type: schema_type)
65
+ @ids_by_schema[key] ||= @upstream.register(subject,
66
+ schema,
67
+ references: references,
68
+ schema_type: schema_type)
69
+ end
55
70
  end
56
71
  end
57
72
  end
@@ -24,9 +24,6 @@ module SchemaRegistry
24
24
  resolv_resolver: nil,
25
25
  retry_limit: nil
26
26
  )
27
- if SchemaRegistry.debug
28
- logger.info("Creating Confluent Schema Registry client with url: #{url}, schema_context: #{schema_context}, user: #{user}, path_prefix: #{path_prefix}")
29
- end
30
27
  @path_prefix = path_prefix
31
28
  @schema_context_prefix = schema_context.nil? ? "" : ":.#{schema_context}:"
32
29
  @schema_context_options = schema_context.nil? ? {} : {query: {subject: @schema_context_prefix}}
@@ -76,7 +73,6 @@ module SchemaRegistry
76
73
  # @param references [Array<Hash>] optional references to other schemas
77
74
  # @return [Integer] the ID of the registered schema
78
75
  def register(subject, schema, references: [], schema_type: "PROTOBUF")
79
- puts("schema_type #{schema_type}")
80
76
  body = {schema: schema.to_s}
81
77
  # Not all schema registry versions support schemaType
82
78
  if schema_type != "AVRO"
@@ -22,7 +22,10 @@ module SchemaRegistry
22
22
  @schema_store ||= SchemaRegistry::AvroSchemaStore.new(
23
23
  path: SchemaRegistry.avro_schema_path || DEFAULT_SCHEMAS_PATH
24
24
  )
25
- @schema_store.load_schemas!
25
+ unless @schemas_loaded
26
+ @schema_store.load_schemas!
27
+ @schemas_loaded = true
28
+ end
26
29
  @schema_store
27
30
  end
28
31
 
@@ -44,10 +47,10 @@ module SchemaRegistry
44
47
  end
45
48
 
46
49
  def decode(stream, schema_text)
47
- # Parse the schema text from the registry into an Avro schema object
48
- JSON.parse(schema_text)
49
- writers_schema = ::Avro::Schema.parse(schema_text)
50
-
50
+ # Cache parsed writer schemas to avoid re-parsing on every decode
51
+ @parsed_writers_schemas ||= {}
52
+ @parsed_writers_schemas[schema_text] ||= ::Avro::Schema.parse(schema_text)
53
+ writers_schema = @parsed_writers_schemas[schema_text]
51
54
  decoder = ::Avro::IO::BinaryDecoder.new(stream)
52
55
 
53
56
  # Try to find the reader schema locally, fall back to writer schema
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchemaRegistry
4
- VERSION = "0.0.11-beta1"
4
+ VERSION = "0.0.11"
5
5
  end
@@ -14,8 +14,6 @@ module SchemaRegistry
14
14
 
15
15
  class << self
16
16
  attr_accessor :avro_schema_path
17
-
18
- attr_accessor :debug
19
17
  end
20
18
 
21
19
  class Client
@@ -136,7 +134,12 @@ module SchemaRegistry
136
134
 
137
135
  def register_schema(message, subject, schema_text: nil, schema_name: nil)
138
136
  schema_text ||= @schema.schema_text(message, schema_name: schema_name)
139
- return if @registry.registered?(schema_text, subject)
137
+
138
+ # Fast path: if already registered, return the cached ID without
139
+ # resolving dependencies again.
140
+ if @registry.registered?(subject, schema_text)
141
+ return @registry.fetch_id(subject, schema_text)
142
+ end
140
143
 
141
144
  # register dependencies first
142
145
  dependencies = @schema.dependencies(message)
@@ -60,18 +60,19 @@ RSpec.describe "decoding" do
60
60
  end
61
61
 
62
62
  describe "with Avro" do
63
- let(:schema_registry_client) do
63
+ around(:each) do |ex|
64
64
  SchemaRegistry.avro_schema_path = "#{__dir__}/schemas"
65
+ ex.run
66
+ SchemaRegistry.avro_schema_path = nil
67
+ end
68
+
69
+ let(:schema_registry_client) do
65
70
  SchemaRegistry::Client.new(
66
71
  registry_url: "http://localhost:8081",
67
72
  schema_type: SchemaRegistry::Schema::Avro.new
68
73
  )
69
74
  end
70
75
 
71
- after do
72
- SchemaRegistry.avro_schema_path = nil
73
- end
74
-
75
76
  it "should decode a simple message" do
76
77
  schema = File.read("#{__dir__}/schemas/simple/v1/SimpleMessage.avsc")
77
78
  stub = stub_request(:get, "http://localhost:8081/schemas/ids/15")
@@ -180,4 +181,121 @@ RSpec.describe "decoding" do
180
181
  end.to raise_error(/Schema|not found/i)
181
182
  end
182
183
  end
184
+
185
+ describe "caching" do
186
+ it "should not fetch the same schema ID twice (Protobuf)" do
187
+ schema = File.read("#{__dir__}/schemas/simple/simple.proto")
188
+ stub = stub_request(:get, "http://localhost:8081/schemas/ids/15")
189
+ .to_return_json(body: {schema: schema})
190
+ msg = Simple::V1::SimpleMessage.new(name: "my name")
191
+ encoded = "\u0000\u0000\u0000\u0000\u000F\u0000#{msg.to_proto}"
192
+
193
+ 3.times { schema_registry_client.decode(encoded) }
194
+
195
+ expect(stub).to have_been_requested.once
196
+ end
197
+
198
+ it "should fetch different schema IDs independently" do
199
+ simple_schema = File.read("#{__dir__}/schemas/simple/simple.proto")
200
+ stub_15 = stub_request(:get, "http://localhost:8081/schemas/ids/15")
201
+ .to_return_json(body: {schema: simple_schema})
202
+
203
+ ref_schema = File.read("#{__dir__}/schemas/referenced/referer.proto")
204
+ stub_20 = stub_request(:get, "http://localhost:8081/schemas/ids/20")
205
+ .to_return_json(body: {schema: ref_schema})
206
+
207
+ msg1 = Simple::V1::SimpleMessage.new(name: "my name")
208
+ encoded1 = "\u0000\u0000\u0000\u0000\u000F\u0000#{msg1.to_proto}"
209
+
210
+ msg2 = Referenced::V1::MessageB::MessageBA.new(
211
+ simple: Simple::V1::SimpleMessage.new(name: "my name")
212
+ )
213
+ encoded2 = "\u0000\u0000\u0000\u0000\u0014\u0004\u0002\u0000#{msg2.to_proto}"
214
+
215
+ # Decode both messages
216
+ schema_registry_client.decode(encoded1)
217
+ schema_registry_client.decode(encoded2)
218
+
219
+ # Each schema fetched exactly once
220
+ expect(stub_15).to have_been_requested.once
221
+ expect(stub_20).to have_been_requested.once
222
+
223
+ # Decode again - no new requests
224
+ schema_registry_client.decode(encoded1)
225
+ schema_registry_client.decode(encoded2)
226
+
227
+ expect(stub_15).to have_been_requested.once
228
+ expect(stub_20).to have_been_requested.once
229
+ end
230
+
231
+ it "should not share cache between different client instances" do
232
+ schema = File.read("#{__dir__}/schemas/simple/simple.proto")
233
+ stub = stub_request(:get, "http://localhost:8081/schemas/ids/15")
234
+ .to_return_json(body: {schema: schema})
235
+ msg = Simple::V1::SimpleMessage.new(name: "my name")
236
+ encoded = "\u0000\u0000\u0000\u0000\u000F\u0000#{msg.to_proto}"
237
+
238
+ schema_registry_client.decode(encoded)
239
+
240
+ # A second client should make its own request
241
+ client2 = SchemaRegistry::Client.new(registry_url: "http://localhost:8081")
242
+ client2.decode(encoded)
243
+
244
+ expect(stub).to have_been_requested.twice
245
+ end
246
+
247
+ describe "with Avro" do
248
+ around(:each) do |ex|
249
+ SchemaRegistry.avro_schema_path = "#{__dir__}/schemas"
250
+ ex.run
251
+ SchemaRegistry.avro_schema_path = nil
252
+ end
253
+
254
+ let(:schema_registry_client) do
255
+ SchemaRegistry::Client.new(
256
+ registry_url: "http://localhost:8081",
257
+ schema_type: SchemaRegistry::Schema::Avro.new
258
+ )
259
+ end
260
+
261
+ it "should not fetch the same schema ID twice" do
262
+ schema = File.read("#{__dir__}/schemas/simple/v1/SimpleMessage.avsc")
263
+ stub = stub_request(:get, "http://localhost:8081/schemas/ids/15")
264
+ .to_return_json(body: {schema: schema})
265
+
266
+ encoded = "\u0000\u0000\u0000\u0000\u000F\u000Emy name"
267
+
268
+ 3.times { schema_registry_client.decode(encoded) }
269
+
270
+ expect(stub).to have_been_requested.once
271
+ end
272
+
273
+ it "should fetch different schema IDs independently" do
274
+ simple_schema = File.read("#{__dir__}/schemas/simple/v1/SimpleMessage.avsc")
275
+ stub_15 = stub_request(:get, "http://localhost:8081/schemas/ids/15")
276
+ .to_return_json(body: {schema: simple_schema})
277
+
278
+ nested_schema = File.read("#{__dir__}/schemas/referenced/v1/MessageBA.avsc")
279
+ stub_20 = stub_request(:get, "http://localhost:8081/schemas/ids/20")
280
+ .to_return_json(body: {schema: nested_schema})
281
+
282
+ encoded1 = "\u0000\u0000\u0000\u0000\u000F\u000Emy name"
283
+ encoded2 = "\u0000\u0000\u0000\u0000\u0014\u000Emy name"
284
+
285
+ # Decode both messages
286
+ schema_registry_client.decode(encoded1)
287
+ schema_registry_client.decode(encoded2)
288
+
289
+ expect(stub_15).to have_been_requested.once
290
+ expect(stub_20).to have_been_requested.once
291
+
292
+ # Decode again - no new requests
293
+ schema_registry_client.decode(encoded1)
294
+ schema_registry_client.decode(encoded2)
295
+
296
+ expect(stub_15).to have_been_requested.once
297
+ expect(stub_20).to have_been_requested.once
298
+ end
299
+ end
300
+ end
183
301
  end
@@ -102,18 +102,19 @@ RSpec.describe "encoding" do
102
102
  end
103
103
 
104
104
  describe "with Avro" do
105
- let(:schema_registry_client) do
105
+ around(:each) do |ex|
106
106
  SchemaRegistry.avro_schema_path = "#{__dir__}/schemas"
107
+ ex.run
108
+ SchemaRegistry.avro_schema_path = nil
109
+ end
110
+
111
+ let(:schema_registry_client) do
107
112
  SchemaRegistry::Client.new(
108
113
  registry_url: "http://localhost:8081",
109
114
  schema_type: SchemaRegistry::Schema::Avro.new
110
115
  )
111
116
  end
112
117
 
113
- after do
114
- SchemaRegistry.avro_schema_path = nil
115
- end
116
-
117
118
  it "should encode a simple message" do
118
119
  schema = File.read("#{__dir__}/schemas/simple/v1/SimpleMessage.avsc")
119
120
  stub = stub_request(:post, "http://localhost:8081/subjects/simple/versions")
@@ -196,4 +197,143 @@ RSpec.describe "encoding" do
196
197
  end.to raise_error(Avro::SchemaValidator::ValidationError)
197
198
  end
198
199
  end
200
+
201
+ describe "caching" do
202
+ it "should not register the same schema twice (Protobuf)" do
203
+ schema = File.read("#{__dir__}/schemas/simple/simple.proto")
204
+ stub = stub_request(:post, "http://localhost:8081/subjects/simple/versions")
205
+ .with(body: {"schemaType" => "PROTOBUF",
206
+ "references" => [],
207
+ "schema" => schema}).to_return_json(body: {id: 15})
208
+ msg = Simple::V1::SimpleMessage.new(name: "my name")
209
+
210
+ 3.times { schema_registry_client.encode(msg, subject: "simple") }
211
+
212
+ expect(stub).to have_been_requested.once
213
+ end
214
+
215
+ it "should register schemas for different subjects separately" do
216
+ schema = File.read("#{__dir__}/schemas/simple/simple.proto")
217
+ stub_a = stub_request(:post, "http://localhost:8081/subjects/subject-a/versions")
218
+ .with(body: {"schemaType" => "PROTOBUF",
219
+ "references" => [],
220
+ "schema" => schema}).to_return_json(body: {id: 15})
221
+ stub_b = stub_request(:post, "http://localhost:8081/subjects/subject-b/versions")
222
+ .with(body: {"schemaType" => "PROTOBUF",
223
+ "references" => [],
224
+ "schema" => schema}).to_return_json(body: {id: 16})
225
+
226
+ msg = Simple::V1::SimpleMessage.new(name: "my name")
227
+
228
+ schema_registry_client.encode(msg, subject: "subject-a")
229
+ schema_registry_client.encode(msg, subject: "subject-b")
230
+
231
+ # Both subjects should get their own registration
232
+ expect(stub_a).to have_been_requested.once
233
+ expect(stub_b).to have_been_requested.once
234
+
235
+ # Encoding again for either subject should not re-register
236
+ schema_registry_client.encode(msg, subject: "subject-a")
237
+ schema_registry_client.encode(msg, subject: "subject-b")
238
+
239
+ expect(stub_a).to have_been_requested.once
240
+ expect(stub_b).to have_been_requested.once
241
+ end
242
+
243
+ it "should not re-register dependencies on subsequent encodes (Protobuf)" do
244
+ schema = File.read("#{__dir__}/schemas/referenced/referer.proto")
245
+ dep_schema = File.read("#{__dir__}/schemas/simple/simple.proto")
246
+ dep_stub = stub_request(:post, "http://localhost:8081/subjects/simple%2Fsimple.proto/versions")
247
+ .with(body: {"schemaType" => "PROTOBUF",
248
+ "references" => [],
249
+ "schema" => dep_schema}).to_return_json(body: {id: 15})
250
+ version_stub = stub_request(:get, "http://localhost:8081/schemas/ids/15/versions")
251
+ .to_return_json(body: [{version: 1, subject: "simple/simple.proto"}])
252
+ stub = stub_request(:post, "http://localhost:8081/subjects/referenced/versions")
253
+ .with(body: {"schemaType" => "PROTOBUF",
254
+ "references" => [
255
+ {
256
+ name: "simple/simple.proto",
257
+ subject: "simple/simple.proto",
258
+ version: 1
259
+ }
260
+ ],
261
+ "schema" => schema}).to_return_json(body: {id: 20})
262
+ msg = Referenced::V1::MessageB::MessageBA.new(
263
+ simple: Simple::V1::SimpleMessage.new(name: "my name")
264
+ )
265
+
266
+ 3.times { schema_registry_client.encode(msg, subject: "referenced") }
267
+
268
+ expect(stub).to have_been_requested.once
269
+ expect(dep_stub).to have_been_requested.once
270
+ expect(version_stub).to have_been_requested.once
271
+ end
272
+
273
+ it "should not share cache between different client instances" do
274
+ schema = File.read("#{__dir__}/schemas/simple/simple.proto")
275
+ stub = stub_request(:post, "http://localhost:8081/subjects/simple/versions")
276
+ .with(body: {"schemaType" => "PROTOBUF",
277
+ "references" => [],
278
+ "schema" => schema}).to_return_json(body: {id: 15})
279
+ msg = Simple::V1::SimpleMessage.new(name: "my name")
280
+
281
+ schema_registry_client.encode(msg, subject: "simple")
282
+
283
+ # A second client should make its own registration request
284
+ client2 = SchemaRegistry::Client.new(registry_url: "http://localhost:8081")
285
+ client2.encode(msg, subject: "simple")
286
+
287
+ expect(stub).to have_been_requested.twice
288
+ end
289
+
290
+ describe "with Avro" do
291
+ around(:each) do |ex|
292
+ SchemaRegistry.avro_schema_path = "#{__dir__}/schemas"
293
+ ex.run
294
+ SchemaRegistry.avro_schema_path = nil
295
+ end
296
+
297
+ let(:schema_registry_client) do
298
+ SchemaRegistry::Client.new(
299
+ registry_url: "http://localhost:8081",
300
+ schema_type: SchemaRegistry::Schema::Avro.new
301
+ )
302
+ end
303
+
304
+ it "should not register the same schema twice" do
305
+ schema = File.read("#{__dir__}/schemas/simple/v1/SimpleMessage.avsc")
306
+ stub = stub_request(:post, "http://localhost:8081/subjects/simple/versions")
307
+ .with(body: {"schema" => schema}).to_return_json(body: {id: 15})
308
+ msg = {"name" => "my name"}
309
+
310
+ 3.times { schema_registry_client.encode(msg, subject: "simple", schema_name: "simple.v1.SimpleMessage") }
311
+
312
+ expect(stub).to have_been_requested.once
313
+ end
314
+
315
+ it "should register schemas for different subjects separately" do
316
+ schema = File.read("#{__dir__}/schemas/simple/v1/SimpleMessage.avsc")
317
+ stub_a = stub_request(:post, "http://localhost:8081/subjects/subject-a/versions")
318
+ .with(body: {"schema" => schema}).to_return_json(body: {id: 15})
319
+ stub_b = stub_request(:post, "http://localhost:8081/subjects/subject-b/versions")
320
+ .with(body: {"schema" => schema}).to_return_json(body: {id: 16})
321
+
322
+ msg = {"name" => "my name"}
323
+
324
+ schema_registry_client.encode(msg, subject: "subject-a", schema_name: "simple.v1.SimpleMessage")
325
+ schema_registry_client.encode(msg, subject: "subject-b", schema_name: "simple.v1.SimpleMessage")
326
+
327
+ expect(stub_a).to have_been_requested.once
328
+ expect(stub_b).to have_been_requested.once
329
+
330
+ # Encoding again should not re-register
331
+ schema_registry_client.encode(msg, subject: "subject-a", schema_name: "simple.v1.SimpleMessage")
332
+ schema_registry_client.encode(msg, subject: "subject-b", schema_name: "simple.v1.SimpleMessage")
333
+
334
+ expect(stub_a).to have_been_requested.once
335
+ expect(stub_b).to have_been_requested.once
336
+ end
337
+ end
338
+ end
199
339
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema_registry_client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.11.pre.beta1
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Orner