avrocadabra 0.0.1
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 +7 -0
- data/CHANGELOG.md +6 -0
- data/Cargo.lock +923 -0
- data/Cargo.toml +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +131 -0
- data/docs/avro_turf.md +61 -0
- data/docs/licenses/crates/quad-rand-0.2.3/DECLARED-LICENSE.txt +32 -0
- data/docs/licenses/gcc-16.2.0/COPYING +340 -0
- data/docs/licenses/gcc-16.2.0/COPYING.RUNTIME +73 -0
- data/docs/licenses/mingw-w64-14.0.0/AUTHORS +73 -0
- data/docs/licenses/mingw-w64-14.0.0/COPYING +43 -0
- data/docs/licenses/rust-1.99.0/COMPILER-BUILTINS.txt +275 -0
- data/docs/licenses/rust-1.99.0/COPYRIGHT-library.html +29357 -0
- data/docs/licenses/rust-1.99.0/LIBM.txt +258 -0
- data/docs/licenses/rust-1.99.0/LLVM-LIBUNWIND.txt +311 -0
- data/docs/licenses/rust-1.99.0/RUST-LICENSE-APACHE.txt +176 -0
- data/docs/licenses/rust-1.99.0/RUST-LICENSE-MIT.txt +25 -0
- data/docs/licenses/rust-1.99.0/STDLIB-BACKTRACE-MIT.txt +25 -0
- data/docs/licenses/rust-1.99.0/STDLIB-PORTABLE-SIMD-MIT.txt +19 -0
- data/docs/licenses/rust-1.99.0/STDLIB-STDARCH-MIT.txt +25 -0
- data/docs/licenses/rust-1.99.0/licenses/Apache-2.0.txt +73 -0
- data/docs/licenses/rust-1.99.0/licenses/BSD-2-Clause.txt +9 -0
- data/docs/licenses/rust-1.99.0/licenses/CC-BY-SA-4.0.txt +427 -0
- data/docs/licenses/rust-1.99.0/licenses/GCC-exception-3.1.txt +30 -0
- data/docs/licenses/rust-1.99.0/licenses/GPL-2.0-only.txt +133 -0
- data/docs/licenses/rust-1.99.0/licenses/GPL-3.0-or-later.txt +202 -0
- data/docs/licenses/rust-1.99.0/licenses/ISC.txt +7 -0
- data/docs/licenses/rust-1.99.0/licenses/LLVM-exception.txt +15 -0
- data/docs/licenses/rust-1.99.0/licenses/MIT.txt +9 -0
- data/docs/licenses/rust-1.99.0/licenses/NCSA.txt +28 -0
- data/docs/licenses/rust-1.99.0/licenses/OFL-1.1.txt +43 -0
- data/docs/licenses/rust-1.99.0/licenses/Unicode-3.0.txt +39 -0
- data/docs/releasing.md +71 -0
- data/docs/third_party.md +5922 -0
- data/ext/avrocadabra/Cargo.toml +29 -0
- data/ext/avrocadabra/build.rs +4 -0
- data/ext/avrocadabra/extconf.rb +9 -0
- data/ext/avrocadabra/src/big_decimal.rs +107 -0
- data/ext/avrocadabra/src/callback.rs +106 -0
- data/ext/avrocadabra/src/convert.rs +617 -0
- data/ext/avrocadabra/src/decode_value.rs +473 -0
- data/ext/avrocadabra/src/guard.rs +680 -0
- data/ext/avrocadabra/src/lib.rs +670 -0
- data/ext/avrocadabra/src/mapping.rs +63 -0
- data/ext/avrocadabra/src/memory.rs +194 -0
- data/ext/avrocadabra/src/prepare.rs +819 -0
- data/ext/avrocadabra/src/resolution.rs +1932 -0
- data/ext/avrocadabra/src/schema_state.rs +60 -0
- data/ext/avrocadabra/src/validation.rs +174 -0
- data/ext/avrocadabra/src/wire.rs +269 -0
- data/lib/avrocadabra/avro_turf/cache.rb +28 -0
- data/lib/avrocadabra/avro_turf/codec.rb +69 -0
- data/lib/avrocadabra/avro_turf/datum_reader.rb +17 -0
- data/lib/avrocadabra/avro_turf/datum_writer.rb +14 -0
- data/lib/avrocadabra/avro_turf/mapping.rb +48 -0
- data/lib/avrocadabra/avro_turf/messaging.rb +12 -0
- data/lib/avrocadabra/avro_turf/ractor_support.rb +154 -0
- data/lib/avrocadabra/avro_turf/routing.rb +15 -0
- data/lib/avrocadabra/avro_turf/schema_state.rb +70 -0
- data/lib/avrocadabra/avro_turf/validation.rb +33 -0
- data/lib/avrocadabra/avro_turf.rb +25 -0
- data/lib/avrocadabra/logical.rb +71 -0
- data/lib/avrocadabra/schema.rb +48 -0
- data/lib/avrocadabra/version.rb +5 -0
- data/lib/avrocadabra.rb +34 -0
- metadata +183 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
mod big_decimal;
|
|
2
|
+
mod callback;
|
|
3
|
+
mod convert;
|
|
4
|
+
mod decode_value;
|
|
5
|
+
mod guard;
|
|
6
|
+
mod mapping;
|
|
7
|
+
mod memory;
|
|
8
|
+
mod prepare;
|
|
9
|
+
mod resolution;
|
|
10
|
+
mod schema_state;
|
|
11
|
+
mod validation;
|
|
12
|
+
mod wire;
|
|
13
|
+
|
|
14
|
+
use apache_avro::{
|
|
15
|
+
Schema, reader::datum::GenericDatumReader, schema::ResolvedSchema,
|
|
16
|
+
writer::datum::GenericDatumWriter,
|
|
17
|
+
};
|
|
18
|
+
use guard::Limits;
|
|
19
|
+
use magnus::{
|
|
20
|
+
DataTypeFunctions, Error, RArray, RModule, RString, Ruby, TypedData, Value, function, gc,
|
|
21
|
+
method, prelude::*, typed_data::Obj, value::Opaque,
|
|
22
|
+
};
|
|
23
|
+
use memory::HeapSize;
|
|
24
|
+
use resolution::Resolution;
|
|
25
|
+
use std::{
|
|
26
|
+
collections::HashMap,
|
|
27
|
+
ffi::c_void,
|
|
28
|
+
io::{self, Write},
|
|
29
|
+
panic::{AssertUnwindSafe, catch_unwind},
|
|
30
|
+
sync::{
|
|
31
|
+
Arc, OnceLock,
|
|
32
|
+
atomic::{AtomicU64, AtomicUsize, Ordering},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
|
37
|
+
|
|
38
|
+
fn push_index(path: &mut String, index: usize) {
|
|
39
|
+
path.push('[');
|
|
40
|
+
path.push_str(index.format_into(&mut std::fmt::NumBuffer::new()));
|
|
41
|
+
path.push(']');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
struct Codec<'a> {
|
|
45
|
+
schema: &'a Schema,
|
|
46
|
+
resolved: ResolvedSchema<'a>,
|
|
47
|
+
writer: GenericDatumWriter<'a>,
|
|
48
|
+
reader: GenericDatumReader<'a>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[ouroboros::self_referencing]
|
|
52
|
+
struct Prepared {
|
|
53
|
+
schemas: Vec<Schema>,
|
|
54
|
+
wire_schemas: Vec<Schema>,
|
|
55
|
+
#[borrows(schemas, wire_schemas)]
|
|
56
|
+
#[covariant]
|
|
57
|
+
codec: Codec<'this>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct Core {
|
|
61
|
+
prepared: Prepared,
|
|
62
|
+
id: u64,
|
|
63
|
+
limits: Limits,
|
|
64
|
+
fields: HashMap<String, usize>,
|
|
65
|
+
memory_size: usize,
|
|
66
|
+
// Plans own no schemas, so reciprocal reader pairs cannot form Arc cycles.
|
|
67
|
+
resolutions: [ResolutionSlot; 8],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[derive(Default)]
|
|
71
|
+
struct ResolutionSlot {
|
|
72
|
+
reader: AtomicU64,
|
|
73
|
+
plan: OnceLock<Arc<Resolution>>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(TypedData)]
|
|
77
|
+
#[magnus(class = "Avrocadabra::NativeSchema", mark, size, frozen_shareable)]
|
|
78
|
+
struct NativeSchema {
|
|
79
|
+
core: Arc<Core>,
|
|
80
|
+
keys: Opaque<RArray>,
|
|
81
|
+
accounted: AtomicUsize,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl DataTypeFunctions for NativeSchema {
|
|
85
|
+
fn free(self: Box<Self>) {
|
|
86
|
+
let bytes = self.accounted.load(Ordering::Relaxed);
|
|
87
|
+
drop(self);
|
|
88
|
+
// Deferred dfree runs under Ruby's GVL; native worker drops never call Ruby.
|
|
89
|
+
unsafe { rb_sys::rb_gc_adjust_memory_usage(-(bytes as isize) as _) };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn size(&self) -> usize {
|
|
93
|
+
size_of::<Self>()
|
|
94
|
+
+ self.core.memory_size
|
|
95
|
+
+ self
|
|
96
|
+
.core
|
|
97
|
+
.resolutions
|
|
98
|
+
.iter()
|
|
99
|
+
.filter_map(|slot| slot.plan.get())
|
|
100
|
+
.map(|plan| plan.memory_size())
|
|
101
|
+
.sum::<usize>()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
fn mark(&self, marker: &gc::Marker) {
|
|
105
|
+
// Pin the array; Ruby traces and relocates its string/symbol entries itself.
|
|
106
|
+
marker.mark(self.keys);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn namespace(ruby: &Ruby) -> Result<RModule, Error> {
|
|
111
|
+
ruby.class_object().const_get("Avrocadabra")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pub(crate) fn error(ruby: &Ruby, class: &str, message: impl AsRef<str>) -> Error {
|
|
115
|
+
match namespace(ruby).and_then(|m| m.const_get(class)) {
|
|
116
|
+
Ok(class) => Error::new(class, message.as_ref().to_owned()),
|
|
117
|
+
Err(error) => error,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn boundary<T>(
|
|
122
|
+
ruby: &Ruby,
|
|
123
|
+
class: &str,
|
|
124
|
+
work: impl FnOnce() -> Result<T, Error>,
|
|
125
|
+
) -> Result<T, Error> {
|
|
126
|
+
catch_unwind(AssertUnwindSafe(work)).unwrap_or_else(|_| {
|
|
127
|
+
Err(error(
|
|
128
|
+
ruby,
|
|
129
|
+
class,
|
|
130
|
+
"native codec rejected input after an internal panic",
|
|
131
|
+
))
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn collect_fields(
|
|
136
|
+
schema: &Schema,
|
|
137
|
+
fields: &mut HashMap<String, usize>,
|
|
138
|
+
limits: Limits,
|
|
139
|
+
) -> Result<(), String> {
|
|
140
|
+
use apache_avro::schema::{InnerDecimalSchema, UuidSchema};
|
|
141
|
+
match schema {
|
|
142
|
+
Schema::Record(record) => {
|
|
143
|
+
for field in &record.fields {
|
|
144
|
+
let next = fields.len();
|
|
145
|
+
fields.entry(field.name.clone()).or_insert(next);
|
|
146
|
+
collect_fields(&field.schema, fields, limits)?;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
Schema::Array(array) => collect_fields(&array.items, fields, limits)?,
|
|
150
|
+
Schema::Map(map) => collect_fields(&map.types, fields, limits)?,
|
|
151
|
+
Schema::Union(union) => {
|
|
152
|
+
for schema in union.variants() {
|
|
153
|
+
collect_fields(schema, fields, limits)?;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
Schema::Fixed(fixed) | Schema::Duration(fixed) | Schema::Uuid(UuidSchema::Fixed(fixed)) => {
|
|
157
|
+
if fixed.size > limits.max_bytes {
|
|
158
|
+
return Err("fixed size exceeds max_bytes".into());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
Schema::Decimal(decimal) => {
|
|
162
|
+
if let InnerDecimalSchema::Fixed(fixed) = &decimal.inner
|
|
163
|
+
&& fixed.size > limits.max_bytes
|
|
164
|
+
{
|
|
165
|
+
return Err("decimal fixed size exceeds max_bytes".into());
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
_ => {}
|
|
169
|
+
}
|
|
170
|
+
Ok(())
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
impl Core {
|
|
174
|
+
fn prepare(json: String, references: Vec<String>, limits: Limits) -> Result<Self, String> {
|
|
175
|
+
let schemas = prepare::schemas(&json, &references, limits)?;
|
|
176
|
+
let wire_schemas = schemas
|
|
177
|
+
.iter()
|
|
178
|
+
.map(wire::schema)
|
|
179
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
180
|
+
let mut fields = HashMap::new();
|
|
181
|
+
for schema in &schemas {
|
|
182
|
+
collect_fields(schema, &mut fields, limits)?;
|
|
183
|
+
}
|
|
184
|
+
let prepared = PreparedTryBuilder {
|
|
185
|
+
schemas,
|
|
186
|
+
wire_schemas,
|
|
187
|
+
codec_builder: move |schemas, wire_schemas| {
|
|
188
|
+
let schema = schemas.last().ok_or("missing root schema")?;
|
|
189
|
+
let resolved = ResolvedSchema::new_with_schemata(schemas.iter().collect())
|
|
190
|
+
.map_err(|e| e.to_string())?;
|
|
191
|
+
let wire_schema = wire_schemas.last().ok_or("missing root wire schema")?;
|
|
192
|
+
let wire_resolved =
|
|
193
|
+
ResolvedSchema::new_with_schemata(wire_schemas.iter().collect())
|
|
194
|
+
.map_err(|error| error.to_string())?;
|
|
195
|
+
let writer = GenericDatumWriter::builder(wire_schema)
|
|
196
|
+
.resolved_schemata(wire_resolved.clone())
|
|
197
|
+
.build()
|
|
198
|
+
.map_err(|e| e.to_string())?;
|
|
199
|
+
let reader = GenericDatumReader::builder(wire_schema)
|
|
200
|
+
.resolved_writer_schemata(wire_resolved)
|
|
201
|
+
.build()
|
|
202
|
+
.map_err(|e| e.to_string())?;
|
|
203
|
+
Ok::<_, String>(Codec {
|
|
204
|
+
schema,
|
|
205
|
+
resolved,
|
|
206
|
+
writer,
|
|
207
|
+
reader,
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
.try_build()?;
|
|
212
|
+
let resolved = &prepared.borrow_codec().resolved;
|
|
213
|
+
let memory_size = size_of::<Self>()
|
|
214
|
+
+ 2 * size_of::<usize>()
|
|
215
|
+
+ 2 * size_of::<Vec<Schema>>()
|
|
216
|
+
+ size_of::<Codec<'_>>()
|
|
217
|
+
+ prepared.borrow_schemas().heap_size()
|
|
218
|
+
+ prepared.borrow_wire_schemas().heap_size()
|
|
219
|
+
+ fields.heap_size()
|
|
220
|
+
+ 3 * (resolved.get_names().heap_size() + size_of_val(resolved.get_schemata()));
|
|
221
|
+
Ok(Self {
|
|
222
|
+
prepared,
|
|
223
|
+
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
|
|
224
|
+
limits,
|
|
225
|
+
fields,
|
|
226
|
+
memory_size,
|
|
227
|
+
resolutions: std::array::from_fn(|_| ResolutionSlot::default()),
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn resolution(&self, reader: &Core) -> Result<Arc<Resolution>, String> {
|
|
232
|
+
for slot in &self.resolutions {
|
|
233
|
+
if slot.reader.load(Ordering::Relaxed) == reader.id
|
|
234
|
+
&& let Some(plan) = slot.plan.get()
|
|
235
|
+
{
|
|
236
|
+
return Ok(Arc::clone(plan));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
let writer = self.prepared.borrow_codec();
|
|
240
|
+
let target = reader.prepared.borrow_codec();
|
|
241
|
+
let limits = self.limits.intersect(reader.limits);
|
|
242
|
+
let plan = Arc::new(Resolution::new(
|
|
243
|
+
writer.schema,
|
|
244
|
+
writer.resolved.get_names(),
|
|
245
|
+
target.schema,
|
|
246
|
+
target.resolved.get_names(),
|
|
247
|
+
limits.max_depth,
|
|
248
|
+
limits.max_items,
|
|
249
|
+
limits.max_bytes,
|
|
250
|
+
)?);
|
|
251
|
+
for slot in &self.resolutions {
|
|
252
|
+
match slot
|
|
253
|
+
.reader
|
|
254
|
+
.compare_exchange(0, reader.id, Ordering::Relaxed, Ordering::Relaxed)
|
|
255
|
+
{
|
|
256
|
+
Ok(_) => {
|
|
257
|
+
// A forked child may have lost the thread initializing this slot.
|
|
258
|
+
let _ = slot.plan.set(Arc::clone(&plan));
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
Err(id) if id == reader.id => {
|
|
262
|
+
return Ok(slot.plan.get().cloned().unwrap_or(plan));
|
|
263
|
+
}
|
|
264
|
+
Err(_) => {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
Ok(plan)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
impl NativeSchema {
|
|
272
|
+
fn account_memory(&self, ruby: &Ruby) {
|
|
273
|
+
let bytes = self.size();
|
|
274
|
+
let previous = self.accounted.fetch_max(bytes, Ordering::Relaxed);
|
|
275
|
+
if bytes > previous {
|
|
276
|
+
ruby.gc_adjust_memory_usage((bytes - previous) as isize);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
fn new(
|
|
281
|
+
ruby: &Ruby,
|
|
282
|
+
json: RString,
|
|
283
|
+
references: RArray,
|
|
284
|
+
depth: usize,
|
|
285
|
+
bytes: usize,
|
|
286
|
+
items: usize,
|
|
287
|
+
) -> Result<Obj<Self>, Error> {
|
|
288
|
+
boundary(ruby, "SchemaError", || {
|
|
289
|
+
if !(1..=128).contains(&depth)
|
|
290
|
+
|| !(1..=67_108_864).contains(&bytes)
|
|
291
|
+
|| !(1..=1_000_000).contains(&items)
|
|
292
|
+
{
|
|
293
|
+
return Err(error(
|
|
294
|
+
ruby,
|
|
295
|
+
"SchemaError",
|
|
296
|
+
"limits require max_depth in 1..128, max_bytes in 1..67108864, max_items in 1..1000000",
|
|
297
|
+
));
|
|
298
|
+
}
|
|
299
|
+
let limits = Limits {
|
|
300
|
+
max_depth: depth,
|
|
301
|
+
max_bytes: bytes,
|
|
302
|
+
max_items: items,
|
|
303
|
+
};
|
|
304
|
+
if references.len() > 65_536 {
|
|
305
|
+
return Err(error(
|
|
306
|
+
ruby,
|
|
307
|
+
"SchemaError",
|
|
308
|
+
"schema reference count exceeds node limit",
|
|
309
|
+
));
|
|
310
|
+
}
|
|
311
|
+
let mut total = 0;
|
|
312
|
+
let json = schema_text(ruby, json, &mut total)?;
|
|
313
|
+
let mut owned_references = Vec::with_capacity(references.len());
|
|
314
|
+
for index in 0..references.len() {
|
|
315
|
+
owned_references.push(schema_text(
|
|
316
|
+
ruby,
|
|
317
|
+
references.entry(index as isize)?,
|
|
318
|
+
&mut total,
|
|
319
|
+
)?);
|
|
320
|
+
}
|
|
321
|
+
let core = Core::prepare(json, owned_references, limits)
|
|
322
|
+
.map_err(|e| error(ruby, "SchemaError", e))?;
|
|
323
|
+
let keys = ruby.ary_new_capa(core.fields.len() * 2);
|
|
324
|
+
for (name, &index) in &core.fields {
|
|
325
|
+
let key = ruby.str_new(name);
|
|
326
|
+
key.freeze();
|
|
327
|
+
keys.store((index * 2) as isize, key)?;
|
|
328
|
+
let symbol: Value = key.funcall("to_sym", ())?;
|
|
329
|
+
keys.store((index * 2 + 1) as isize, symbol)?;
|
|
330
|
+
}
|
|
331
|
+
keys.freeze();
|
|
332
|
+
let ractor: magnus::RClass = ruby.class_object().const_get("Ractor")?;
|
|
333
|
+
let keys: RArray = ractor.funcall("make_shareable", (keys,))?;
|
|
334
|
+
let schema = ruby.obj_wrap(Self {
|
|
335
|
+
core: Arc::new(core),
|
|
336
|
+
keys: keys.into(),
|
|
337
|
+
accounted: AtomicUsize::new(0),
|
|
338
|
+
});
|
|
339
|
+
schema.account_memory(ruby);
|
|
340
|
+
Ok(schema)
|
|
341
|
+
})
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
fn encode(
|
|
345
|
+
ruby: &Ruby,
|
|
346
|
+
this: &Self,
|
|
347
|
+
value: Value,
|
|
348
|
+
release_gvl: bool,
|
|
349
|
+
graph: Option<RArray>,
|
|
350
|
+
) -> Result<RString, Error> {
|
|
351
|
+
boundary(ruby, "EncodeError", || {
|
|
352
|
+
let mapping = graph.map(mapping::Mapping::new).transpose()?;
|
|
353
|
+
let value =
|
|
354
|
+
convert::encode(ruby, &this.core, value, ruby.get_inner(this.keys), mapping)?;
|
|
355
|
+
let core = Arc::clone(&this.core);
|
|
356
|
+
let bytes = pure(ruby, release_gvl, "EncodeError", move || {
|
|
357
|
+
let mut output = BoundedWriter {
|
|
358
|
+
bytes: Vec::new(),
|
|
359
|
+
limit: core.limits.max_bytes,
|
|
360
|
+
};
|
|
361
|
+
core.prepared
|
|
362
|
+
.borrow_codec()
|
|
363
|
+
.writer
|
|
364
|
+
.write_ser(&mut output, &wire::Datum(value))
|
|
365
|
+
.map_err(|e| e.to_string())?;
|
|
366
|
+
Ok(output.bytes)
|
|
367
|
+
})?;
|
|
368
|
+
Ok(ruby.str_from_slice(&bytes))
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fn decode(
|
|
373
|
+
ruby: &Ruby,
|
|
374
|
+
this: &Self,
|
|
375
|
+
input: Value,
|
|
376
|
+
reader: Option<&Self>,
|
|
377
|
+
release_gvl: bool,
|
|
378
|
+
tagged_unions: bool,
|
|
379
|
+
graph: Option<RArray>,
|
|
380
|
+
) -> Result<Value, Error> {
|
|
381
|
+
boundary(ruby, "DecodeError", || {
|
|
382
|
+
let target = reader.unwrap_or(this);
|
|
383
|
+
let limits = this.core.limits.intersect(target.core.limits);
|
|
384
|
+
let (bytes, offset, stream) = match RString::from_value(input) {
|
|
385
|
+
Some(bytes) => (bytes, 0, None),
|
|
386
|
+
None => {
|
|
387
|
+
let class: magnus::RClass = ruby.class_object().const_get("StringIO")?;
|
|
388
|
+
if !input.is_kind_of(class) {
|
|
389
|
+
return Err(error(
|
|
390
|
+
ruby,
|
|
391
|
+
"DecodeError",
|
|
392
|
+
"input must be a String or StringIO",
|
|
393
|
+
));
|
|
394
|
+
}
|
|
395
|
+
let _: Value = input.funcall("read", (0,))?;
|
|
396
|
+
(
|
|
397
|
+
input.funcall("string", ())?,
|
|
398
|
+
input.funcall("pos", ())?,
|
|
399
|
+
Some(input),
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
let codec = this.core.prepared.borrow_codec();
|
|
404
|
+
let slice = unsafe { bytes.as_slice() }
|
|
405
|
+
.get(offset..)
|
|
406
|
+
.unwrap_or_default();
|
|
407
|
+
let frame = guard::inspect(codec.schema, codec.resolved.get_names(), slice, limits)
|
|
408
|
+
.map_err(|e| error(ruby, "DecodeError", e))?;
|
|
409
|
+
let consumed = frame.consumed;
|
|
410
|
+
// No Ruby-owned memory crosses the GVL boundary.
|
|
411
|
+
let bytes = slice[..consumed].to_vec();
|
|
412
|
+
let core = Arc::clone(&this.core);
|
|
413
|
+
let plan = reader
|
|
414
|
+
.filter(|reader| reader.core.id != core.id)
|
|
415
|
+
.map(|reader| core.resolution(&reader.core))
|
|
416
|
+
.transpose()
|
|
417
|
+
.map_err(|e| error(ruby, "ResolutionError", e))?;
|
|
418
|
+
if plan.is_some() {
|
|
419
|
+
this.account_memory(ruby);
|
|
420
|
+
}
|
|
421
|
+
let value = pure(ruby, release_gvl, "DecodeError", move || {
|
|
422
|
+
let codec = core.prepared.borrow_codec();
|
|
423
|
+
let wire::Datum(value) = codec
|
|
424
|
+
.reader
|
|
425
|
+
.read_deser(&mut bytes.as_slice())
|
|
426
|
+
.map_err(|e| e.to_string())?;
|
|
427
|
+
let value = wire::materialize(
|
|
428
|
+
value,
|
|
429
|
+
codec.schema,
|
|
430
|
+
codec.resolved.get_names(),
|
|
431
|
+
&mut frame.unions.into_iter(),
|
|
432
|
+
)?;
|
|
433
|
+
Ok(match plan {
|
|
434
|
+
Some(plan) => plan.apply(value),
|
|
435
|
+
None => Ok(resolution::Resolved {
|
|
436
|
+
value,
|
|
437
|
+
defaults: Vec::new(),
|
|
438
|
+
adapters: Vec::new(),
|
|
439
|
+
failure: None,
|
|
440
|
+
}),
|
|
441
|
+
})
|
|
442
|
+
})?
|
|
443
|
+
.map_err(|e| error(ruby, "ResolutionError", e))?;
|
|
444
|
+
let codec = target.core.prepared.borrow_codec();
|
|
445
|
+
let value = decode_value::decode(
|
|
446
|
+
ruby,
|
|
447
|
+
codec.schema,
|
|
448
|
+
codec.resolved.get_names(),
|
|
449
|
+
value.value,
|
|
450
|
+
ruby.get_inner(target.keys),
|
|
451
|
+
&target.core.fields,
|
|
452
|
+
decode_value::Options {
|
|
453
|
+
limits,
|
|
454
|
+
tagged_unions,
|
|
455
|
+
mapping: graph.map(mapping::Mapping::new).transpose()?,
|
|
456
|
+
defaults: value.defaults,
|
|
457
|
+
adapters: value.adapters,
|
|
458
|
+
failure: value.failure,
|
|
459
|
+
},
|
|
460
|
+
)?;
|
|
461
|
+
if let Some(stream) = stream {
|
|
462
|
+
let _: Value = stream.funcall("pos=", (offset + consumed,))?;
|
|
463
|
+
}
|
|
464
|
+
Ok(value)
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
fn schema_text(ruby: &Ruby, value: RString, total: &mut usize) -> Result<String, Error> {
|
|
470
|
+
*total = total
|
|
471
|
+
.checked_add(value.len())
|
|
472
|
+
.filter(|&n| n <= 1024 * 1024)
|
|
473
|
+
.ok_or_else(|| error(ruby, "SchemaError", "schemas exceed 1 MiB"))?;
|
|
474
|
+
unsafe { value.as_str() }
|
|
475
|
+
.map(str::to_owned)
|
|
476
|
+
.map_err(|e| error(ruby, "SchemaError", e.to_string()))
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
struct BoundedWriter {
|
|
480
|
+
bytes: Vec<u8>,
|
|
481
|
+
limit: usize,
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
impl Write for BoundedWriter {
|
|
485
|
+
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
|
486
|
+
let end = self
|
|
487
|
+
.bytes
|
|
488
|
+
.len()
|
|
489
|
+
.checked_add(bytes.len())
|
|
490
|
+
.filter(|&n| n <= self.limit)
|
|
491
|
+
.ok_or_else(|| io::Error::other("encoded datum exceeds max_bytes"))?;
|
|
492
|
+
if end > self.bytes.capacity() {
|
|
493
|
+
let capacity = end
|
|
494
|
+
.max(self.bytes.capacity().saturating_mul(2))
|
|
495
|
+
.min(self.limit);
|
|
496
|
+
self.bytes
|
|
497
|
+
.try_reserve_exact(capacity - self.bytes.len())
|
|
498
|
+
.map_err(io::Error::other)?;
|
|
499
|
+
}
|
|
500
|
+
self.bytes.extend_from_slice(bytes);
|
|
501
|
+
Ok(bytes.len())
|
|
502
|
+
}
|
|
503
|
+
fn flush(&mut self) -> io::Result<()> {
|
|
504
|
+
Ok(())
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn pure<T: Send, F: FnOnce() -> Result<T, String> + Send>(
|
|
509
|
+
ruby: &Ruby,
|
|
510
|
+
release: bool,
|
|
511
|
+
class: &str,
|
|
512
|
+
work: F,
|
|
513
|
+
) -> Result<T, Error> {
|
|
514
|
+
let work = || {
|
|
515
|
+
catch_unwind(AssertUnwindSafe(work))
|
|
516
|
+
.unwrap_or_else(|_| Err("native codec rejected input after an internal panic".into()))
|
|
517
|
+
};
|
|
518
|
+
if !release {
|
|
519
|
+
return work().map_err(|e| error(ruby, class, e));
|
|
520
|
+
}
|
|
521
|
+
struct Job<F, T> {
|
|
522
|
+
work: Option<F>,
|
|
523
|
+
result: Option<Result<T, String>>,
|
|
524
|
+
}
|
|
525
|
+
unsafe extern "C" fn run<F: FnOnce() -> Result<T, String>, T>(
|
|
526
|
+
data: *mut c_void,
|
|
527
|
+
) -> *mut c_void {
|
|
528
|
+
// `protect` below keeps this stack frame alive through Ruby interrupts.
|
|
529
|
+
let job = unsafe { &mut *data.cast::<Job<F, T>>() };
|
|
530
|
+
if let Some(work) = job.work.take() {
|
|
531
|
+
job.result = Some(work());
|
|
532
|
+
}
|
|
533
|
+
std::ptr::null_mut()
|
|
534
|
+
}
|
|
535
|
+
fn invoke<F: FnOnce() -> Result<T, String>, T>(job: &mut Job<F, T>) {
|
|
536
|
+
unsafe {
|
|
537
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
538
|
+
Some(run::<F, T>),
|
|
539
|
+
(job as *mut Job<F, T>).cast(),
|
|
540
|
+
None,
|
|
541
|
+
std::ptr::null_mut(),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
let mut job = Job {
|
|
546
|
+
work: Some(work),
|
|
547
|
+
result: None,
|
|
548
|
+
};
|
|
549
|
+
magnus::rb_sys::protect(|| {
|
|
550
|
+
invoke(&mut job);
|
|
551
|
+
rb_sys::Qnil as rb_sys::VALUE
|
|
552
|
+
})?;
|
|
553
|
+
job.result
|
|
554
|
+
.ok_or_else(|| error(ruby, class, "native work was interrupted"))?
|
|
555
|
+
.map_err(|e| error(ruby, class, e))
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#[magnus::init]
|
|
559
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
560
|
+
validation::initialize()
|
|
561
|
+
.map_err(|message| Error::new(ruby.exception_runtime_error(), message))?;
|
|
562
|
+
unsafe { rb_sys::rb_ext_ractor_safe(true) };
|
|
563
|
+
let module = ruby.define_module("Avrocadabra")?;
|
|
564
|
+
let class = module.define_class("NativeSchema", ruby.class_object())?;
|
|
565
|
+
module.define_class("NativeCallback", ruby.class_object())?;
|
|
566
|
+
callback::Callback::class(ruby);
|
|
567
|
+
let _: Value = module.funcall("private_constant", ("NativeCallback",))?;
|
|
568
|
+
// Magnus's lazy class lookup can deadlock with Ruby GC across Ractor GVLs.
|
|
569
|
+
NativeSchema::class(ruby);
|
|
570
|
+
class.define_singleton_method("new", function!(NativeSchema::new, 5))?;
|
|
571
|
+
class.define_singleton_method("unchanged?", function!(schema_state::unchanged, 2))?;
|
|
572
|
+
class.define_method("encode", method!(NativeSchema::encode, 3))?;
|
|
573
|
+
class.define_method("decode", method!(NativeSchema::decode, 5))?;
|
|
574
|
+
Ok(())
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#[cfg(test)]
|
|
578
|
+
mod tests {
|
|
579
|
+
use super::*;
|
|
580
|
+
use apache_avro::types::Value as AvroValue;
|
|
581
|
+
|
|
582
|
+
fn core(schema: &str) -> Core {
|
|
583
|
+
Core::prepare(
|
|
584
|
+
schema.into(),
|
|
585
|
+
Vec::new(),
|
|
586
|
+
Limits {
|
|
587
|
+
max_depth: 64,
|
|
588
|
+
max_bytes: 1024,
|
|
589
|
+
max_items: 1024,
|
|
590
|
+
},
|
|
591
|
+
)
|
|
592
|
+
.unwrap()
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
#[test]
|
|
596
|
+
fn resolution_skips_incomplete_slots_inherited_at_fork() {
|
|
597
|
+
let writer = core(r#""int""#);
|
|
598
|
+
let reader = core(r#""long""#);
|
|
599
|
+
for slot in &writer.resolutions {
|
|
600
|
+
slot.reader.store(reader.id, Ordering::Relaxed);
|
|
601
|
+
}
|
|
602
|
+
let plan = writer.resolution(&reader).unwrap();
|
|
603
|
+
assert_eq!(
|
|
604
|
+
plan.apply(AvroValue::Int(7)).unwrap().value,
|
|
605
|
+
AvroValue::Long(7)
|
|
606
|
+
);
|
|
607
|
+
assert!(
|
|
608
|
+
writer
|
|
609
|
+
.resolutions
|
|
610
|
+
.iter()
|
|
611
|
+
.all(|slot| slot.plan.get().is_none())
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
#[test]
|
|
616
|
+
fn concurrent_resolution_reserves_one_slot_per_reader() {
|
|
617
|
+
let writer = core(r#""int""#);
|
|
618
|
+
let reader = core(r#""long""#);
|
|
619
|
+
std::thread::scope(|scope| {
|
|
620
|
+
for _ in 0..16 {
|
|
621
|
+
scope.spawn(|| {
|
|
622
|
+
let plan = writer.resolution(&reader).unwrap();
|
|
623
|
+
assert_eq!(
|
|
624
|
+
plan.apply(AvroValue::Int(7)).unwrap().value,
|
|
625
|
+
AvroValue::Long(7)
|
|
626
|
+
);
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
assert_eq!(
|
|
631
|
+
writer
|
|
632
|
+
.resolutions
|
|
633
|
+
.iter()
|
|
634
|
+
.filter(|slot| slot.plan.get().is_some())
|
|
635
|
+
.count(),
|
|
636
|
+
1
|
|
637
|
+
);
|
|
638
|
+
assert!(Arc::ptr_eq(
|
|
639
|
+
&writer.resolution(&reader).unwrap(),
|
|
640
|
+
&writer.resolution(&reader).unwrap()
|
|
641
|
+
));
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
#[test]
|
|
645
|
+
fn resolution_cache_is_bounded_without_replacing_published_plans() {
|
|
646
|
+
let writer = core(r#""int""#);
|
|
647
|
+
let first_reader = core(r#""long""#);
|
|
648
|
+
let first_plan = writer.resolution(&first_reader).unwrap();
|
|
649
|
+
for _ in 0..16 {
|
|
650
|
+
let reader = core(r#""long""#);
|
|
651
|
+
let plan = writer.resolution(&reader).unwrap();
|
|
652
|
+
assert_eq!(
|
|
653
|
+
plan.apply(AvroValue::Int(7)).unwrap().value,
|
|
654
|
+
AvroValue::Long(7)
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
assert_eq!(
|
|
658
|
+
writer
|
|
659
|
+
.resolutions
|
|
660
|
+
.iter()
|
|
661
|
+
.filter(|slot| slot.plan.get().is_some())
|
|
662
|
+
.count(),
|
|
663
|
+
8
|
|
664
|
+
);
|
|
665
|
+
assert!(Arc::ptr_eq(
|
|
666
|
+
&first_plan,
|
|
667
|
+
&writer.resolution(&first_reader).unwrap()
|
|
668
|
+
));
|
|
669
|
+
}
|
|
670
|
+
}
|