Move de/serialization to ruby for easier error detection.

Closes open transactions if de/serialization fails for any reason,
avoiding the object to be in a potentially confused state.

Thanks to Michael Granger (ged@faeriemud.org) for the heads up and test case.

FossilOrigin-Name: e6e52675510533da8a26b0e2f0b2f73505a3b4ee0c94b123c37089489ed7745a
This commit is contained in:
Mahlon E. Smith 2021-10-06 20:43:13 +00:00
parent 6cc96d8fae
commit 25655d0554
4 changed files with 56 additions and 29 deletions

View file

@ -1 +1 @@
2.7.2
2.7

View file

@ -135,11 +135,7 @@ rmdbx_key_for( VALUE key, MDBX_val *ckey )
void
rmdbx_val_for( VALUE self, VALUE val, MDBX_val *data )
{
VALUE serialize_proc = rb_iv_get( self, "@serializer" );
if ( ! NIL_P( serialize_proc ) )
val = rb_funcall( serialize_proc, rb_intern("call"), 1, val );
val = rb_funcall( self, rb_intern("serialize"), 1, val );
Check_Type( val, T_STRING );
data->iov_len = RSTRING_LEN( val );
@ -148,20 +144,6 @@ rmdbx_val_for( VALUE self, VALUE val, MDBX_val *data )
}
/*
* Deserialize and return a value.
*/
VALUE
rmdbx_deserialize( VALUE self, VALUE val )
{
VALUE deserialize_proc = rb_iv_get( self, "@deserializer" );
if ( ! NIL_P( deserialize_proc ) )
val = rb_funcall( deserialize_proc, rb_intern("call"), 1, val );
return val;
}
/*
* Open the DB environment handle.
*
@ -355,7 +337,7 @@ rmdbx_get_val( VALUE self, VALUE key )
switch ( rc ) {
case MDBX_SUCCESS:
rv = rb_str_new( data.iov_base, data.iov_len );
return rmdbx_deserialize( self, rv );
return rb_funcall( self, rb_intern("deserialize"), 1, rv );
case MDBX_NOTFOUND:
return Qnil;
@ -446,6 +428,10 @@ rmdbx_set_subdb( VALUE self, VALUE name )
size_t len = RSTRING_LEN( name ) + 1;
db->subdb = malloc( len );
strlcpy( db->subdb, StringValuePtr(name), len );
rmdbx_log_obj( self, "debug", "setting subdb: %s", RSTRING_PTR(name) );
}
else {
rmdbx_log_obj( self, "debug", "clearing subdb" );
}
/* Reset the db handle and issue a single transaction to reify
@ -644,11 +630,11 @@ rmdbx_each_value_i( VALUE self )
if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
VALUE rv = rb_str_new( data.iov_base, data.iov_len );
rb_yield( rmdbx_deserialize( self, rv ) );
rb_yield( rb_funcall( self, rb_intern("deserialize"), 1, rv ) );
while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
rv = rb_str_new( data.iov_base, data.iov_len );
rb_yield( rmdbx_deserialize( self, rv ) );
rb_yield( rb_funcall( self, rb_intern("deserialize"), 1, rv ) );
}
}
@ -694,12 +680,15 @@ rmdbx_each_pair_i( VALUE self )
if ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_FIRST ) == MDBX_SUCCESS ) {
VALUE rkey = rb_str_new( key.iov_base, key.iov_len );
VALUE rval = rb_str_new( data.iov_base, data.iov_len );
rb_yield( rb_assoc_new( rkey, rmdbx_deserialize( self, rval ) ) );
rval = rb_funcall( self, rb_intern("deserialize"), 1, rval );
rb_yield( rb_assoc_new( rkey, rval ) );
while ( mdbx_cursor_get( db->cursor, &key, &data, MDBX_NEXT ) == MDBX_SUCCESS ) {
rkey = rb_str_new( key.iov_base, key.iov_len );
rval = rb_str_new( data.iov_base, data.iov_len );
rb_yield( rb_assoc_new( rkey, rmdbx_deserialize( self, rval ) ) );
rval = rb_funcall( self, rb_intern("deserialize"), 1, rval );
rb_yield( rb_assoc_new( rkey, rval ) );
}
}

View file

@ -364,6 +364,34 @@ class MDBX::Database
protected
#########
### Safely serialize a value, closing any open transaction and re-raising
### if necessary.
###
def serialize( val )
return val unless self.serializer
return self.serializer.call( val )
rescue => err
self.close_transaction( false )
raise err
end
### Safely deserialize a value, closing any open transaction and re-raising
### if necessary.
###
def deserialize( val )
return val unless self.deserializer
return self.deserializer.call( val )
rescue => err
self.close_transaction( false )
raise err
end
### Yield and return the block, opening a snapshot first if
### there isn't already a transaction in progress. Closes
### the snapshot if this method opened it.

View file

@ -265,12 +265,22 @@ RSpec.describe( MDBX::Database ) do
it "reverts back to previous collection if the block raises an exception" do
expect( db.collection ).to be_nil
begin
expect {
db.collection( 'bucket1' ) do
db.collection( 'bucket2' ) { raise "ka-bloooey!" }
end
rescue
}.to raise_error( RuntimeError, /ka-bloooey!/ )
expect( db.collection ).to be_nil
end
it "reverts back to previous collection if serialization fails" do
db.serializer = ->( v ) { raise "ka-bloooey!" }
expect( db.collection ).to be_nil
expect {
db.collection( 'bucket1' ) do
db.collection( 'bucket2' ) {|sdb| sdb['foo'] = 1 }
end
}.to raise_error( RuntimeError, /ka-bloooey!/ )
expect( db.collection ).to be_nil
end