1package App::Netdisco::DB::ExplicitLocking;
2
3use strict;
4use warnings;
5
6our %lock_modes;
7
8BEGIN {
9  %lock_modes = (
10    ACCESS_SHARE => 'ACCESS SHARE',
11    ROW_SHARE => 'ROW SHARE',
12    ROW_EXCLUSIVE => 'ROW EXCLUSIVE',
13    SHARE_UPDATE_EXCLUSIVE => 'SHARE UPDATE EXCLUSIVE',
14    SHARE => 'SHARE',
15    SHARE_ROW_EXCLUSIVE => 'SHARE ROW EXCLUSIVE',
16    EXCLUSIVE => 'EXCLUSIVE',
17    ACCESS_EXCLUSIVE => 'ACCESS EXCLUSIVE',
18  );
19}
20
21use constant \%lock_modes;
22
23use base 'Exporter';
24our @EXPORT = ();
25our @EXPORT_OK = (keys %lock_modes);
26our %EXPORT_TAGS = (modes => \@EXPORT_OK);
27
28sub txn_do_locked {
29  my ($self, $table, $mode, $sub) = @_;
30  my $sql_fmt = q{LOCK TABLE %s IN %%s MODE};
31  my $schema = $self;
32
33  if ($self->can('result_source')) {
34      # ResultSet component
35      $sub = $mode;
36      $mode = $table;
37      $table = $self->result_source->from;
38      $schema = $self->result_source->schema;
39  }
40
41  $schema->throw_exception('missing Table name to txn_do_locked()')
42    unless $table;
43
44  $table = [$table] if ref '' eq ref $table;
45  my $table_fmt = join ', ', ('%s' x scalar @$table);
46  my $sql = sprintf $sql_fmt, $table_fmt;
47
48  if (ref '' eq ref $mode and $mode) {
49      scalar grep {$_ eq $mode} values %lock_modes
50        or $schema->throw_exception('bad LOCK_MODE to txn_do_locked()');
51  }
52  else {
53      $sub = $mode;
54      $mode = 'ACCESS EXCLUSIVE';
55  }
56
57  $schema->txn_do(sub {
58      my @params = map {$schema->storage->dbh->quote_identifier($_)} @$table;
59      $schema->storage->dbh->do(sprintf $sql, @params, $mode);
60      $sub->();
61  });
62}
63
64=head1 NAME
65
66App::Netdisco::DB::ExplicitLocking - Support for PostgreSQL Lock Modes
67
68=head1 SYNOPSIS
69
70In your L<DBIx::Class> schema:
71
72 package My::Schema;
73 __PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');
74
75Then, in your application code:
76
77 use App::Netdisco::DB::ExplicitLocking ':modes';
78 $schema->txn_do_locked($table, MODE_NAME, sub { ... });
79
80This also works for the ResultSet:
81
82 package My::Schema::ResultSet::TableName;
83 __PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');
84
85Then, in your application code:
86
87 use App::Netdisco::DB::ExplicitLocking ':modes';
88 $schema->resultset('TableName')->txn_do_locked(MODE_NAME, sub { ... });
89
90=head1 DESCRIPTION
91
92This L<DBIx::Class> component provides an easy way to execute PostgreSQL table
93locks before a transaction block.
94
95You can load the component in either the Schema class or ResultSet class (or
96both) and then use an interface very similar to C<DBIx::Class>'s C<txn_do()>.
97
98The package also exports constants for each of the table lock modes supported
99by PostgreSQL, which must be used if specifying the mode (default mode is
100C<ACCESS EXCLUSIVE>).
101
102=head1 EXPORTS
103
104With the C<:modes> tag (as in SYNOPSIS above) the following constants are
105exported and must be used if specifying the lock mode:
106
107=over 4
108
109=item * C<ACCESS_SHARE>
110
111=item * C<ROW_SHARE>
112
113=item * C<ROW_EXCLUSIVE>
114
115=item * C<SHARE_UPDATE_EXCLUSIVE>
116
117=item * C<SHARE>
118
119=item * C<SHARE_ROW_EXCLUSIVE>
120
121=item * C<EXCLUSIVE>
122
123=item * C<ACCESS_EXCLUSIVE>
124
125=back
126
127=head1 METHODS
128
129=head2 C<< $schema->txn_do_locked($table|\@tables, MODE_NAME?, $subref) >>
130
131This is the method signature used when the component is loaded into your
132Schema class. The reason you might want to use this over the ResultSet version
133(below) is to specify multiple tables to be locked before the transaction.
134
135The first argument is one or more tables, and is required. Note that these are
136the real table names in PostgreSQL, and not C<DBIx::Class> ResultSet aliases
137or anything like that.
138
139The mode name is optional, and defaults to C<ACCESS EXCLUSIVE>. You must use
140one of the exported constants in this parameter.
141
142Finally pass a subroutine reference, just as you would to the normal
143C<DBIx::Class> C<txn_do()> method. Note that additional arguments are not
144supported.
145
146=head2 C<< $resultset->txn_do_locked(MODE_NAME?, $subref) >>
147
148This is the method signature used when the component is loaded into your
149ResultSet class. If you don't yet have a ResultSet class (which is the default
150- normally only Result classes are created) then you can create a stub which
151simply loads this component (and inherits from C<DBIx::Class::ResultSet>).
152
153This is the simplest way to use this module if you only want to lock one table
154before your transaction block.
155
156The first argument is the optional mode name, which defaults to C<ACCESS
157EXCLUSIVE>. You must use one of the exported constants in this parameter.
158
159The second argument is a subroutine reference, just as you would pass to the
160normal C<DBIx::Class> C<txn_do()> method. Note that additional arguments are
161not supported.
162
163=cut
164
1651;
166