#!/usr/bin/perl package KiokuDB::LiveObjects; use Moose; use Scalar::Util qw(weaken); use KiokuDB::LiveObjects::Guard; use Hash::Util::FieldHash::Compat qw(fieldhash); use Carp qw(croak); BEGIN { local $@; eval 'use Devel::PartialDump qw(croak)' }; use Set::Object; use KiokuDB::LiveObjects::Scope; use KiokuDB::LiveObjects::TXNScope; use namespace::clean -except => 'meta'; has _objects => ( isa => "HashRef", is => "ro", init_arg => undef, default => sub { fieldhash my %hash }, ); has _ids => ( #metaclass => 'Collection::Hash', isa => "HashRef", is => "ro", init_arg => undef, default => sub { return {} }, #provides => { # get => "ids_to_objects", # keys => "live_ids", # values => "live_objects", #}, ); sub id_to_object { my ( $self, $id ) = @_; $self->_ids->{$id}; } sub ids_to_objects { my ( $self, @ids ) = @_; @{ $self->_ids }{@ids}; } sub live_ids { keys %{ shift->_ids }; } sub live_objects { values %{ shift->_ids }; } has _entry_objects => ( isa => "HashRef", is => "ro", init_arg => undef, default => sub { fieldhash my %hash }, ); has _entry_ids => ( #metaclass => 'Collection::Hash', isa => "HashRef", is => "ro", init_arg => undef, default => sub { return {} }, #provides => { # get => "ids_to_entries", # keys => "loaded_ids", # values => "live_entries", #}, ); sub id_to_entry { my ( $self, $id ) = @_; $self->_entry_ids->{$id}; } sub ids_to_entries { my ( $self, @ids ) = @_; @{ $self->_entry_ids }{@ids} } sub loaded_ids { keys %{ shift->_entry_ids }; } sub live_entries { values %{ shift->_entry_ids }; } has current_scope => ( isa => "KiokuDB::LiveObjects::Scope", is => "ro", writer => "_set_current_scope", clearer => "_clear_current_scope", weak_ref => 1, ); has txn_scope => ( isa => "KiokuDB::LiveObjects::TXNScope", is => "ro", writer => "_set_txn_scope", clearer => "_clear_txn_scope", weak_ref => 1, ); sub new_scope { my $self = shift; my $parent = $self->current_scope; my $child = KiokuDB::LiveObjects::Scope->new( ( $parent ? ( parent => $parent ) : () ), live_objects => $self, ); $self->_set_current_scope($child); return $child; } sub new_txn { my $self = shift; my $parent = $self->txn_scope; my $child = KiokuDB::LiveObjects::TXNScope->new( ( $parent ? ( parent => $parent ) : () ), live_objects => $self, ); $self->_set_txn_scope($child); return $child; } sub objects_to_ids { my ( $self, @objects ) = @_; return $self->object_to_id($objects[0]) if @objects == 1; my $o = $self->_objects; return map { my $ent = $o->{$_}; $ent && $ent->{id}; } @objects; } sub object_to_id { my ( $self, $obj ) = @_; if ( my $ent = $self->_objects->{$obj} ){ return $ent->{id}; } return undef; } sub objects_to_entries { my ( $self, @objects ) = @_; return $self->object_to_entry($objects[0]) if @objects == 1; my $o = $self->_objects; return map { my $ent = $o->{$_}; $ent && $ent->{entry}; } @objects; } sub object_to_entry { my ( $self, $obj ) = @_; if ( my $ent = $self->_objects->{$obj} ){ return $ent->{entry}; } return undef; } sub update_entries { my ( $self, @entries ) = @_; my @ret; my ( $o, $i, $eo, $ei ) = ( $self->_objects, $self->_ids, $self->_entry_objects, $self->_entry_ids ); foreach my $entry ( @entries ) { my $id = $entry->id; push @ret, $ei->{$id} if defined wantarray; weaken($ei->{$id} = $entry); $eo->{$entry} ||= KiokuDB::LiveObjects::Guard->new( $ei, $id ); if ( ref(my $obj = $i->{$id}) ) { my $ent = $o->{$obj}; $ent->{entry} = $entry; } elsif ( ref( my $object = $entry->object ) ) { $self->insert( $entry => $object ); } } if ( my $s = $self->txn_scope ) { $s->update_entries(@entries); } @ret; } sub rollback_entries { my ( $self, @entries ) = @_; my ( $o, $i, $ei ) = ( $self->_objects, $self->_ids, $self->_entry_ids ); foreach my $entry ( reverse @entries ) { my $id = $entry->id; if ( my $prev = $entry->prev ) { $ei->{$id} = $prev; my $obj = $i->{$id}; $o->{$obj}{entry} = $prev; } else { delete $ei->{$id}; delete $i->{$id}; } } } sub remove { my ( $self, @stuff ) = @_; my ( $o, $i, $eo, $ei ) = ( $self->_objects, $self->_ids, $self->_entry_objects, $self->_entry_ids ); foreach my $thing ( @stuff ) { if ( ref $thing ) { delete $o->{$thing}; # guard invokes delete $eo->{$thing}; # in case it's a ref, same deal } else { if ( ref( my $object = delete $i->{$thing} ) ) { if ( my $ent = delete $o->{$object} ) { $ent->{guard}->dismiss; } } if ( my $entry = $ei->{$thing} ) { delete($eo->{$entry})->dismiss; } } } } sub insert { my ( $self, @pairs ) = @_; croak "The arguments must be an list of pairs of IDs/Entries to objects" unless @pairs % 2 == 0; my ( $o, $i, $eo, $ei ) = ( $self->_objects, $self->_ids, $self->_entry_objects, $self->_entry_ids ); my $s = $self->current_scope or croak "no open live object scope"; while ( @pairs ) { my ( $id, $object ) = splice @pairs, 0, 2; my $entry; if ( ref $id ) { $entry = $id; $id = $entry->id; } confess("blah") unless $id; croak($object, " is not a reference") unless ref($object); croak($object, " is an entry") if blessed($object) && $object->isa("KiokuDB::Entry"); croak($object, " is already registered as $o->{$object}{id}") if exists $o->{$object} and $o->{$object}{id} ne $id;; if ( exists $i->{$id} ) { croak "An object with the id '$id' is already registered"; } else { weaken($i->{$id} = $object); $s->push($object); if ( $entry and !$ei->{$id} ) { $ei->{$id} = $entry; $eo->{$entry} = KiokuDB::LiveObjects::Guard->new( $ei, $id ); } # note, $entry = $e->{$id} is *not* desired, it isn't necessarily # up to date $o->{$object} = { id => $id, entry => $entry, guard => KiokuDB::LiveObjects::Guard->new( $i, $id ), }; } } } sub insert_entries { my ( $self, @entries ) = @_; confess "non reference entries: ", join ", ", map { $_ ? $_ : "undef" } @entries if grep { !ref } @entries; my $i = $self->_ids; @entries = grep { not exists $i->{$_->id} } @entries; my @ids = map { $_->id } @entries; my $ei = $self->_entry_ids; @{ $self->_entry_objects }{@entries} = map { KiokuDB::LiveObjects::Guard->new( $ei, $_ ) } @ids; weaken($_) for @{$ei}{@ids} = @entries; return; } sub clear { my $self = shift; foreach my $ent ( values %{ $self->_objects } ) { if ( my $guard = $ent->{guard} ) { # sometimes gone in global destruction $guard->dismiss; } } foreach my $guard ( values %{ $self->_entry_objects } ) { next unless $guard; $guard->dismiss; } # avoid the now needless weaken magic, should be faster %{ $self->_objects } = (); %{ $self->_ids } = (); %{ $self->_entry_ids } = (); %{ $self->_entry_objects } = (); } __PACKAGE__->meta->make_immutable; __PACKAGE__ __END__ =pod =head1 NAME KiokuDB::LiveObjects - Live object set tracking =head1 SYNOPSIS $live_objects->insert( $entry => $object ); $live_objects->insert( $id => $object ); my $id = $live_objects->object_to_id( $object ); my $obj = $live_objects->id_to_object( $id ); my $scope = $live_objects->new_scope; =head1 DESCRIPTION This object keeps track of the set of live objects, their associated IDs, and the storage entries. =head1 METHODS =over 4 =item insert Takes pairs, id or entry as the key, and object as the value, registering the objects. =item insert_entries Takes entries and registers them without an object. This is used when prefetching entries, before their objects are actually inflated. =item objects_to_ids =item object_to_id Given objects, returns their IDs, or undef for objects which not registered. =item objects_to_entries =item object_to_entry Given objects, find the corresponding entries. =item ids_to_objects =item id_to_object Given IDs, find the corresponding objects. =item ids_to_entries Given IDs, find the corresponding entries. =item update_entries Given entries, replaces the live entries of the corresponding objects with the newly updated ones. The objects must already be in the live object set. This method is called on a successful transaction commit. =item new_scope Creates a new L, with the current scope as its parent. =item current_scope The current L instance. This is the scope into which newly registered objects are pushed. =item new_txn Creates a new L, with the current txn scope as its parent. =item txn_scope The current L. =item clear Forces a clear of the live object set. This removes all objects and entries, and can be useful in the case of leaks (to prevent false positives on lookups). Note that this does not actually break the circular structures, so the leak is unresolved, but the objects are no longer considered live by the L instance. =item live_entries =item live_objects =item live_ids Enumerates the live entries, objects or ids. =item rollback_entries Called by L. =item remove Removes entries from the live object set. =back =cut