Skip to content

Commit 5026128

Browse files
committed
Introduce M.A.D. within the schema/source instance linkage
When a source instance is registered with a schema instance, the code in Schema::_register_source() adds a strong ref of the source to the schema register, and a weak schema ref to the source itself. Install DESTROY handlers both on Schema and ResultSource to flip this setup any time the Schema instance goes out of scope (if we somehow increment the refcount of $self in a DESTROY, then the garbage collection is aborted). Tested all the way back to 5.8.1 with excellent results. Promote the source links in both ResultSet and Row to real ResultSource instance refs, as there's no longer any issue with memory leaks (before a source handle would be instantiated lazily to sidestep the source object entirely by keeping a schema ref instead). Add freeze/thaw hooks for proper serialization of Source-containing structures (be it Row's or ResultSet's). In another round of cleanup ResultSourceHandle will be reduced to purely a "no schema yet" placeholder after schema-less a freeze/thaw cycle.
1 parent c76e526 commit 5026128

File tree

7 files changed

+154
-70
lines changed

7 files changed

+154
-70
lines changed

Changes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
Revision history for DBIx::Class
22

3+
* New Features / Changes
4+
- Schema/resultsource instances are now crossreferenced via a new
5+
system guaranteeing leak-free mutually assuered destruction
6+
37
* Fixes
48
- Revert default selection to being lazy again (eagerness introduced
59
in 0.08125) - fixes DBIx::Class::Helper::ResultSet::RemoveColumns

lib/DBIx/Class/ResultSet.pm

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use base qw/DBIx::Class/;
66
use Carp::Clan qw/^DBIx::Class/;
77
use DBIx::Class::Exception;
88
use Data::Page;
9-
use Storable;
109
use DBIx::Class::ResultSetColumn;
1110
use DBIx::Class::ResultSourceHandle;
1211
use Hash::Merge ();
@@ -31,7 +30,7 @@ use overload
3130
'bool' => "_bool",
3231
fallback => 1;
3332

34-
__PACKAGE__->mk_group_accessors('simple' => qw/_result_class _source_handle/);
33+
__PACKAGE__->mk_group_accessors('simple' => qw/_result_class result_source/);
3534

3635
=head1 NAME
3736
@@ -197,8 +196,8 @@ sub new {
197196
return $class->new_result(@_) if ref $class;
198197

199198
my ($source, $attrs) = @_;
200-
$source = $source->handle
201-
unless $source->isa('DBIx::Class::ResultSourceHandle');
199+
$source = $source->resolve
200+
if $source->isa('DBIx::Class::ResultSourceHandle');
202201
$attrs = { %{$attrs||{}} };
203202

204203
if ($attrs->{page}) {
@@ -210,16 +209,16 @@ sub new {
210209
# Creation of {} and bless separated to mitigate RH perl bug
211210
# see https://bugzilla.redhat.com/show_bug.cgi?id=196836
212211
my $self = {
213-
_source_handle => $source,
212+
result_source => $source,
214213
cond => $attrs->{where},
215214
pager => undef,
216-
attrs => $attrs
215+
attrs => $attrs,
217216
};
218217

219218
bless $self, $class;
220219

221220
$self->result_class(
222-
$attrs->{result_class} || $source->resolve->result_class
221+
$attrs->{result_class} || $source->result_class
223222
);
224223

225224
return $self;
@@ -2278,7 +2277,6 @@ sub new_result {
22782277
@$cols_from_relations
22792278
? (-cols_from_relations => $cols_from_relations)
22802279
: (),
2281-
-source_handle => $self->_source_handle,
22822280
-result_source => $self->result_source, # DO NOT REMOVE THIS, REQUIRED
22832281
);
22842282

@@ -3615,17 +3613,6 @@ sub _merge_joinpref_attr {
36153613
}
36163614
}
36173615

3618-
sub result_source {
3619-
my $self = shift;
3620-
3621-
if (@_) {
3622-
$self->_source_handle($_[0]->handle);
3623-
} else {
3624-
$self->_source_handle->resolve;
3625-
}
3626-
}
3627-
3628-
36293616
sub STORABLE_freeze {
36303617
my ($self, $cloning) = @_;
36313618
my $to_serialize = { %$self };
@@ -3655,8 +3642,8 @@ See L<DBIx::Class::Schema/throw_exception> for details.
36553642
sub throw_exception {
36563643
my $self=shift;
36573644

3658-
if (ref $self && $self->_source_handle->schema) {
3659-
$self->_source_handle->schema->throw_exception(@_)
3645+
if (ref $self and my $rsrc = $self->result_source) {
3646+
$rsrc->throw_exception(@_)
36603647
}
36613648
else {
36623649
DBIx::Class::Exception->throw(@_);

lib/DBIx/Class/ResultSource.pm

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use DBIx::Class::Exception;
1010
use Carp::Clan qw/^DBIx::Class/;
1111
use Try::Tiny;
1212
use List::Util 'first';
13+
use Scalar::Util qw/weaken isweak/;
14+
use Storable qw/nfreeze thaw/;
1315
use namespace::clean;
1416

1517
use base qw/DBIx::Class/;
@@ -1742,6 +1744,56 @@ sub handle {
17421744
});
17431745
}
17441746

1747+
{
1748+
my $global_phase_destroy;
1749+
1750+
END { $global_phase_destroy++ }
1751+
1752+
sub DESTROY {
1753+
return if $global_phase_destroy;
1754+
1755+
######
1756+
# !!! ACHTUNG !!!!
1757+
######
1758+
#
1759+
# Under no circumstances shall $_[0] be stored anywhere else (like copied to
1760+
# a lexical variable, or shifted, or anything else). Doing so will mess up
1761+
# the refcount of this particular result source, and will allow the $schema
1762+
# we are trying to save to reattach back to the source we are destroying.
1763+
# The relevant code checking refcounts is in ::Schema::DESTROY()
1764+
1765+
# if we are not a schema instance holder - we don't matter
1766+
return if(
1767+
! ref $_[0]->{schema}
1768+
or
1769+
isweak $_[0]->{schema}
1770+
);
1771+
1772+
# weaken our schema hold forcing the schema to find somewhere else to live
1773+
weaken $_[0]->{schema};
1774+
1775+
# if schema is still there reintroduce ourselves with strong refs back
1776+
if ($_[0]->{schema}) {
1777+
my $srcregs = $_[0]->{schema}->source_registrations;
1778+
for (keys %$srcregs) {
1779+
$srcregs->{$_} = $_[0] if $srcregs->{$_} == $_[0];
1780+
}
1781+
}
1782+
}
1783+
}
1784+
1785+
sub STORABLE_freeze {
1786+
my ($self, $cloning) = @_;
1787+
nfreeze($self->handle);
1788+
}
1789+
1790+
sub STORABLE_thaw {
1791+
my ($self, $cloning, $ice) = @_;
1792+
%$self = %{ (thaw $ice)->resolve };
1793+
}
1794+
1795+
1796+
17451797
=head2 throw_exception
17461798
17471799
See L<DBIx::Class::Schema/"throw_exception">.

lib/DBIx/Class/Row.pm

Lines changed: 23 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ BEGIN {
2121
: sub () { 0 };
2222
}
2323

24-
__PACKAGE__->mk_group_accessors('simple' => qw/_source_handle/);
24+
__PACKAGE__->mk_group_accessors('simple' => [result_source => '_result_source']);
2525

2626
=head1 NAME
2727
@@ -64,12 +64,12 @@ this class, you are better off calling it on a
6464
L<DBIx::Class::ResultSet> object.
6565
6666
When calling it directly, you will not get a complete, usable row
67-
object until you pass or set the C<source_handle> attribute, to a
67+
object until you pass or set the C<result_source> attribute, to a
6868
L<DBIx::Class::ResultSource> instance that is attached to a
6969
L<DBIx::Class::Schema> with a valid connection.
7070
7171
C<$attrs> is a hashref of column name, value data. It can also contain
72-
some other attributes such as the C<source_handle>.
72+
some other attributes such as the C<result_source>.
7373
7474
Passing an object, or an arrayref of objects as a value will call
7575
L<DBIx::Class::Relationship::Base/set_from_related> for you. When
@@ -160,19 +160,14 @@ sub new {
160160
my ($class, $attrs) = @_;
161161
$class = ref $class if ref $class;
162162

163-
my $new = {
164-
_column_data => {},
165-
};
166-
bless $new, $class;
167-
168-
if (my $handle = delete $attrs->{-source_handle}) {
169-
$new->_source_handle($handle);
170-
}
163+
my $new = bless { _column_data => {} }, $class;
171164

172-
my $source;
173-
if ($source = delete $attrs->{-result_source}) {
174-
$new->result_source($source);
175-
}
165+
my $source =
166+
delete $attrs->{-result_source}
167+
or
168+
( $attrs->{-source_handle} and (delete $attrs->{-source_handle})->resolve )
169+
;
170+
$new->result_source($source) if $source;
176171

177172
if (my $related = delete $attrs->{-cols_from_relations}) {
178173
@{$new->{_ignore_at_insert}={}}{@$related} = ();
@@ -821,9 +816,13 @@ sub _is_column_numeric {
821816
my $colinfo = $self->column_info ($column);
822817

823818
# cache for speed (the object may *not* have a resultsource instance)
824-
if (! defined $colinfo->{is_numeric} && $self->_source_handle) {
819+
if (
820+
! defined $colinfo->{is_numeric}
821+
and
822+
my $storage = try { $self->result_source->schema->storage }
823+
) {
825824
$colinfo->{is_numeric} =
826-
$self->result_source->schema->storage->is_datatype_numeric ($colinfo->{data_type})
825+
$storage->is_datatype_numeric ($colinfo->{data_type})
827826
? 1
828827
: 0
829828
;
@@ -1142,20 +1141,13 @@ L<DBIx::Class::ResultSet>, see L<DBIx::Class::ResultSet/result_class>.
11421141
sub inflate_result {
11431142
my ($class, $source, $me, $prefetch) = @_;
11441143

1145-
my ($source_handle) = $source;
1146-
1147-
if ($source->isa('DBIx::Class::ResultSourceHandle')) {
1148-
$source = $source_handle->resolve
1149-
}
1150-
else {
1151-
$source_handle = $source->handle
1152-
}
1144+
$source = $source->resolve
1145+
if $source->isa('DBIx::Class::ResultSourceHandle');
11531146

1154-
my $new = {
1155-
_source_handle => $source_handle,
1156-
_column_data => $me,
1157-
};
1158-
bless $new, (ref $class || $class);
1147+
my $new = bless
1148+
{ _column_data => $me, _result_source => $source },
1149+
ref $class || $class
1150+
;
11591151

11601152
foreach my $pre (keys %{$prefetch||{}}) {
11611153

@@ -1290,26 +1282,14 @@ sub is_column_changed {
12901282
12911283
=over
12921284
1293-
=item Arguments: none
1285+
=item Arguments: $result_source_instance
12941286
12951287
=item Returns: a ResultSource instance
12961288
12971289
=back
12981290
12991291
Accessor to the L<DBIx::Class::ResultSource> this object was created from.
13001292
1301-
=cut
1302-
1303-
sub result_source {
1304-
my $self = shift;
1305-
1306-
if (@_) {
1307-
$self->_source_handle($_[0]->handle);
1308-
} else {
1309-
$self->_source_handle->resolve;
1310-
}
1311-
}
1312-
13131293
=head2 register_column
13141294
13151295
$column_info = { .... };

lib/DBIx/Class/Schema.pm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use File::Spec;
1111
use Sub::Name 'subname';
1212
use Module::Find();
1313
use Storable();
14+
use B qw/svref_2object/;
1415
use namespace::clean;
1516

1617
use base qw/DBIx::Class/;
@@ -1372,6 +1373,29 @@ sub _register_source {
13721373
$self->class_mappings(\%map);
13731374
}
13741375

1376+
{
1377+
my $global_phase_destroy;
1378+
1379+
END { $global_phase_destroy++ }
1380+
1381+
sub DESTROY {
1382+
return if $global_phase_destroy;
1383+
1384+
my $self = shift;
1385+
my $srcs = $self->source_registrations;
1386+
1387+
for my $moniker (keys %$srcs) {
1388+
# find first source that is not about to be GCed (someone other than $self
1389+
# holds a reference to it) and reattach to it, weakening our own link
1390+
if (ref $srcs->{$moniker} and svref_2object($srcs->{$moniker})->REFCNT > 1) {
1391+
$srcs->{$moniker}->schema($self);
1392+
weaken $srcs->{$moniker};
1393+
last;
1394+
}
1395+
}
1396+
}
1397+
}
1398+
13751399
sub _unregister_source {
13761400
my ($self, $moniker) = @_;
13771401
my %reg = %{$self->source_registrations};

t/52leaks.t

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
use strict;
2-
use warnings;
3-
41
# Do the override as early as possible so that CORE::bless doesn't get compiled away
52
# We will replace $bless_override only if we are in author mode
63
my $bless_override;
@@ -11,6 +8,8 @@ BEGIN {
118
*CORE::GLOBAL::bless = sub { goto $bless_override };
129
}
1310

11+
use strict;
12+
use warnings;
1413
use Test::More;
1514

1615
use lib qw(t/lib);
@@ -171,11 +170,47 @@ unless (DBICTest::RunMode->is_plain) {
171170
$weak_registry->{"basic $_"} = { weakref => $base_collection->{$_} };
172171
weaken $weak_registry->{"basic $_"}{weakref};
173172
}
174-
175173
}
176174

177-
memory_cycle_ok($weak_registry, 'No cycles in the weakened object collection')
178-
if $have_test_cycle;
175+
# check that "phantom-chaining" works - we never lose track of the original $schema
176+
# and have access to the entire tree without leaking anything
177+
{
178+
my $phantom;
179+
for (
180+
sub { DBICTest->init_schema },
181+
sub { shift->source('Artist') },
182+
sub { shift->resultset },
183+
sub { shift->result_source },
184+
sub { shift->schema },
185+
sub { shift->resultset('Artist') },
186+
sub { shift->find_or_create({ name => 'detachable' }) },
187+
sub { shift->result_source },
188+
sub { shift->schema },
189+
sub { shift->clone },
190+
sub { shift->resultset('Artist') },
191+
sub { shift->next },
192+
sub { shift->result_source },
193+
sub { shift->resultset },
194+
sub { shift->create({ name => 'detached' }) },
195+
sub { shift->update({ name => 'reattached' }) },
196+
sub { shift->discard_changes },
197+
sub { shift->delete },
198+
sub { shift->insert },
199+
) {
200+
$phantom = $_->($phantom);
201+
202+
my $slot = (sprintf 'phantom %s=%s(0x%x)', # so we don't trigger stringification
203+
ref $phantom,
204+
reftype $phantom,
205+
refaddr $phantom,
206+
);
207+
$weak_registry->{$slot} = $phantom;
208+
weaken $weak_registry->{$slot};
209+
}
210+
211+
ok( $phantom->in_storage, 'Properly deleted/reinserted' );
212+
is( $phantom->name, 'reattached', 'Still correct name' );
213+
}
179214

180215
# Naturally we have some exceptions
181216
my $cleared;

xt/podcoverage.t

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ my $exceptions = {
6868
resolve_condition
6969
resolve_join
7070
resolve_prefetch
71+
STORABLE_freeze
72+
STORABLE_thaw
7173
/],
7274
},
7375
'DBIx::Class::ResultSet' => {

0 commit comments

Comments
 (0)