ensembl-hive  2.6
DBConnection.pm
Go to the documentation of this file.
1 =pod
2 
3 =head1 NAME
4 
6 
7 =head1 SYNOPSIS
8 
9  my $url = $dbc->url();
10 
11 =head1 DESCRIPTION
12 
13  Extends the functionality of Bio::EnsEMBL::Hive::DBSQL::CoreDBConnection with things needed by the Hive
14 
15 =head1 LICENSE
16 
17  Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
18  Copyright [2016-2024] EMBL-European Bioinformatics Institute
19 
20  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
21  You may obtain a copy of the License at
22 
23  http://www.apache.org/licenses/LICENSE-2.0
24 
25  Unless required by applicable law or agreed to in writing, software distributed under the License
26  is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
27  See the License for the specific language governing permissions and limitations under the License.
28 
29 =head1 CONTACT
30 
31  Please subscribe to the Hive mailing list: http://listserver.ebi.ac.uk/mailman/listinfo/ehive-users to discuss Hive-related questions or to be notified of our updates
32 
33 =cut
34 
35 
36 package Bio::EnsEMBL::Hive::DBSQL::DBConnection;
37 
38 use strict;
39 use warnings;
40 
41 use Time::HiRes ('usleep');
42 use Bio::EnsEMBL::Hive::Utils ('throw');
43 use Bio::EnsEMBL::Hive::Utils::URL ('parse', 'hash_to_url');
45 
46 use base ('Bio::EnsEMBL::Hive::DBSQL::CoreDBConnection');
47 
48 
49 sub new {
50  my $class = shift;
51  my %flags = @_;
52 
53  if(my $url = delete $flags{'-url'}) {
54  if(my $parsed_url = Bio::EnsEMBL::Hive::Utils::URL::parse( $url )) {
55 
56  foreach my $name ( 'driver', 'host', 'port', 'user', 'pass', 'dbname' ) {
57  $flags{ "-$name" } //= $parsed_url->{$name};
58  }
59  foreach my $name ( keys %{$parsed_url->{'conn_params'}} ) {
60  $flags{ "-$name" } //= $parsed_url->{'conn_params'}->{$name};
61  }
62 
63  return $class->SUPER::new( %flags );
64 
65  } else {
66  throw("Could not create DBC because could not parse the URL '$url'");
67  }
68  } else {
69  return $class->SUPER::new( @_ );
70  }
71 }
72 
73 
74 sub _optional_pair { # helper function
75  my ($key, $value) = @_;
76 
77  return defined($value) ? ($key => $value) : ();
78 }
79 
80 
81 sub to_url_hash {
82  my ($self, $psw_env_var_name) = @_;
83 
84  my $psw_expression;
85  if($psw_expression = $self->password) {
86  if($psw_env_var_name) {
87  $ENV{$psw_env_var_name} = $psw_expression;
88  $psw_expression = '${'.$psw_env_var_name.'}';
89  }
90  }
91 
92  my $url_hash = {
93  _optional_pair('driver', $self->driver),
94  _optional_pair('user', $self->username),
95  _optional_pair('pass', $psw_expression),
96  _optional_pair('host', $self->host),
97  _optional_pair('port', $self->port),
98  _optional_pair('dbname', $self->dbname),
99 
100  'conn_params' => {
101  _optional_pair('disconnect_when_inactive', $self->disconnect_when_inactive),
102  _optional_pair('wait_timeout', $self->wait_timeout),
103  _optional_pair('reconnect_when_lost', $self->reconnect_when_lost),
104  },
105  };
106 
107  return $url_hash;
108 }
109 
110 
111 =head2 url
112 
113  Arg [1] : String $environment_variable_name_to_store_password_in (optional)
114  Example : $url = $dbc->url;
115  Description: Constructs a URL string for this database connection.
116  Returntype : string of format mysql://<user>:<pass>@<host>:<port>/<dbname>
117  or sqlite:
118  Exceptions : none
119  Caller : general
120 
121 =cut
122 
123 sub url {
124  my ($self, $psw_env_var_name) = @_;
125 
126  return Bio::EnsEMBL::Hive::Utils::URL::hash_to_url( $self->to_url_hash( $psw_env_var_name ) );
127 }
128 
129 
130 sub connect { # a wrapper that imitates CSMA/CD protocol's incremental backoff-and-retry approach
131  my $self = shift @_;
132 
133  my $attempts = 9;
134  my $sleep_sec = 30;
135  my $retval;
136 
137  foreach my $attempt (1..$attempts) {
138  eval {
139  $retval = $self->SUPER::connect( @_ );
140  1;
141  } or do {
142  if (Bio::EnsEMBL::Hive::Utils::SQLErrorParser::is_server_too_busy($self->driver, $@)) {
143 
144  warn "Possibly transient problem conecting to the database (attempt #$attempt). Will try again in $sleep_sec sec";
145 
146  usleep( $sleep_sec*1000000 );
147  $sleep_sec *= 2;
148  next;
149 
150  } else { # but definitely report other errors
151 
152  die $@;
153  }
154  };
155  last; # stop looping once we succeeded
156  }
157 
158  if($@) {
159  die "After $attempts attempts still could not connect() : $@";
160  }
161 
162  return $retval;
163 }
164 
165 
166 sub protected_prepare_execute { # try to resolve certain mysql "Deadlocks" by imitating CSMA/CD protocol's incremental backoff-and-retry approach (a useful workaround even in mysql 5.1.61)
167  my $self = shift @_;
168  my $sql_params = shift @_;
169  my $deadlock_log_callback = shift @_;
170 
171  my $sql_cmd = shift @$sql_params;
172 
173  my $attempts = 9;
174  my $sleep_max_sec = 1;
175 
176  my $retval;
177  my $query_msg;
178 
179  foreach my $attempt (1..$attempts) {
180  eval {
181  my $sth = $self->prepare( $sql_cmd );
182  $retval = $sth->execute( @$sql_params );
183  $sth->finish;
184  1;
185  } or do {
186  $query_msg = "QUERY: $sql_cmd, PARAMS: (".join(', ',@$sql_params).")";
187 
189 
190  my $this_sleep_sec = int( rand( $sleep_max_sec )*100 ) / 100.0;
191 
192  if( $deadlock_log_callback ) {
193  $deadlock_log_callback->( " temporarily failed due to a DEADLOCK in the database (attempt #$attempt). Will try again in $this_sleep_sec sec" );
194  }
195 
196  usleep( $this_sleep_sec*1000000 );
197  $sleep_max_sec *= 2;
198  next;
199 
200  } else { # but definitely report other errors
201 
202  die "$@ -- $query_msg";
203  }
204  };
205  last; # stop looping once we succeeded
206  }
207 
208  die "After $attempts attempts the query $query_msg still cannot be run: $@" if($@);
209 
210  return $retval;
211 }
212 
213 
214 our $pass_internal_counter = 0;
215 sub to_cmd {
216  my ($self, $executable, $prepend, $append, $sqlcmd, $hide_password_in_env) = @_;
217 
218  my $driver = $self->driver || 'mysql';
219 
220  my $dbname = $self->dbname;
221  if($sqlcmd) {
222  if($sqlcmd =~ /(DROP\s+DATABASE(?:\s+IF\s+EXISTS)?\s*?)(?:\s+(\w+))?/i) {
223  $dbname = $2 if $2;
224 
225  if($driver eq 'sqlite') {
226  return ['rm', '-f', $dbname];
227  } else {
228  if (not $dbname) {
229  die "'DROP DATABASE' needs a database name\n";
230  }
231  if ($driver eq 'mysql') {
232  $sqlcmd = "$1 \`$dbname\`" unless $2;
233  } else {
234  $sqlcmd = "$1 $dbname" unless $2;
235  }
236  $dbname = '';
237  }
238  } elsif($sqlcmd =~ /(CREATE\s+DATABASE(?:\s+IF\s+NOT\s+EXISTS)?\s*?)(?:\s+(\w+))?/i ) {
239  $dbname = $2 if $2;
240 
241  if($driver eq 'sqlite') {
242  return ['touch', $dbname];
243  } else {
244  if (not $dbname) {
245  die "'CREATE DATABASE' needs a database name\n";
246  }
247  my %limits = ( 'mysql' => 64, 'pgsql' => 63 );
248  if (length($dbname) > $limits{$driver}) {
249  die "Database name '$dbname' is too long (> $limits{$driver}). Cannot create the database\n";
250  }
251  if ($driver eq 'mysql') {
252  $sqlcmd = "$1 \`$dbname\`" unless $2;
253  } else {
254  $sqlcmd = "$1 $dbname" unless $2;
255  }
256  $dbname = '';
257  }
258  }
259  }
260 
261  my @cmd;
262 
263  my $hidden_password;
264  if ($self->password) {
265  if ($hide_password_in_env) {
266  my $pass_variable = "EHIVE_TMP_PASSWORD_${pass_internal_counter}";
267  $pass_internal_counter++;
268  $ENV{$pass_variable} = $self->password;
269  $hidden_password = '$'.$pass_variable;
270  } else {
271  $hidden_password = $self->password;
272  }
273  }
274 
275  if($driver eq 'mysql') {
276  $executable ||= 'mysql';
277 
278  push @cmd, ('env', 'MYSQL_PWD='.$hidden_password) if ($self->password);
279  push @cmd, $executable;
280  push @cmd, @$prepend if ($prepend && @$prepend);
281  push @cmd, '--host='.$self->host if $self->host;
282  push @cmd, '--port='.$self->port if $self->port;
283  push @cmd, '--user='.$self->username if $self->username;
284 # push @cmd, '--password='.$hidden_password if $self->password;
285  push @cmd, ('-e', $sqlcmd) if $sqlcmd;
286  push @cmd, $dbname if $dbname;
287 
288  } elsif($driver eq 'pgsql') {
289  $executable ||= 'psql';
290 
291  push @cmd, ('env', 'PGPASSWORD='.$hidden_password) if ($self->password);
292  push @cmd, $executable;
293  push @cmd, @$prepend if ($prepend && @$prepend);
294  push @cmd, ('-h', $self->host) if defined($self->host);
295  push @cmd, ('-p', $self->port) if defined($self->port);
296  push @cmd, ('-U', $self->username) if defined($self->username);
297  push @cmd, ('-c', $sqlcmd) if $sqlcmd;
298  push @cmd, $dbname if $dbname;
299 
300  } elsif($driver eq 'sqlite') {
301  $executable ||= 'sqlite3';
302 
303  die "sqlite requires a database (file) name\n" unless $dbname;
304 
305  push @cmd, $executable;
306  push @cmd, @$prepend if ($prepend && @$prepend);
307  push @cmd, $dbname;
308  push @cmd, $sqlcmd if $sqlcmd;
309  }
310 
311  push @cmd, @$append if ($append && @$append);
312 
313  return \@cmd;
314 }
315 
316 
317 =head2 run_in_transaction
318 
319  Description : Wrapper that first sets AutoCommit to 0, runs some user code, and at the end issues a commit() / rollback()
320  It also has to temporarily set disconnect_when_inactive() to 1 because a value of 0 would cause the
321  DBConnection object to disconnect early, which would rollback the transaction.
322  NB: This is essentially a trimmed copy of Ensembl's Utils::SqlHelper::transaction()
323 
324 =cut
325 
326 sub run_in_transaction {
327  my ($self, $callback) = @_;
328 
329  # Save the original value of disconnect_when_inactive()
330  my $original_dwi = $self->disconnect_when_inactive();
331  $self->disconnect_when_inactive(0);
332 
333  $self->reconnect() unless $self->db_handle()->ping();
334 
335  # Save the original value of "AutoCommit"
336  my $original_ac = $self->db_handle()->{'AutoCommit'};
337  $self->db_handle()->{'AutoCommit'} = 0;
338 
339  my $result;
340  eval {
341  $result = $callback->();
342  # FIXME: does this work if the "MySQL server has gone away" ?
343  $self->db_handle()->commit();
344  };
345  my $error = $@;
346 
347  #If there is an error then we apply rollbacks
348  if($error) {
349  eval { $self->db_handle()->rollback(); };
350  }
351 
352  # Restore the original values
353  $self->db_handle()->{'AutoCommit'} = $original_ac;
354  $self->disconnect_when_inactive($original_dwi);
355 
356  die "ABORT: Transaction aborted because of error: ${error}" if $error;
357  return $result;
358 }
359 
360 
361 =head2 has_write_access
362 
363  Example : my $can_do = $dbc->has_write_access();
364  Description : Tells whether the underlying database connection has write access to the database
365  Returntype : Boolean
366  Exceptions : none
367  Caller : general
368  Status : Stable
369 
370 =cut
371 
372 sub has_write_access {
373  my $self = shift;
374  if ($self->driver eq 'mysql') {
375  my $current_user = $self->selectrow_arrayref('SELECT CURRENT_USER()');
376  # munge grantee - user and host specification need single quoting
377  my $grantee = join '@', map { qq{'$_'} } split /@/, @$current_user[0];
378  # instance wide privileges
379  my $access_sql =
380  q{SELECT COUNT(*)
381  FROM information_schema.user_privileges
382  WHERE PRIVILEGE_TYPE IN ('INSERT', 'DELETE', 'UPDATE')
383  AND GRANTEE = ?};
384  my $user_entries =
385  $self->selectall_arrayref($access_sql, undef, $grantee);
386  # schema specific privileges
387  $access_sql =
388  q{SELECT COUNT(*)
389  FROM information_schema.schema_privileges
390  WHERE PRIVILEGE_TYPE IN ('INSERT', 'DELETE', 'UPDATE')
391  AND DATABASE() LIKE TABLE_SCHEMA
392  AND GRANTEE = ?};
393  my $schema_entries =
394  $self->selectall_arrayref($access_sql, undef, $grantee);
395  my $has_write_access_from_some_host = 0;
396  foreach my $entry (@$user_entries, @$schema_entries) {
397  $has_write_access_from_some_host ||= !!(3 == @$entry[0]);
398  }
399  return $has_write_access_from_some_host;
400  } elsif ($self->driver eq 'pgsql') {
401  my $access_sql =
402  q{SELECT COUNT(*)
403  FROM (SELECT DISTINCT PRIVILEGE_TYPE
404  FROM information_schema.table_privileges
405  WHERE PRIVILEGE_TYPE IN ('INSERT', 'DELETE', 'UPDATE')
406  AND GRANTEE = current_user
407  AND TABLE_CATALOG = current_database()) AS temp};
408  my $user_entries =
409  $self->selectall_arrayref($access_sql, undef);
410  my $has_write_access_from_some_host = 0;
411  foreach my $entry (@$user_entries) {
412  $has_write_access_from_some_host ||= !!(3 == @$entry[0]);
413  }
414  return $has_write_access_from_some_host;
415  } else {
416  # TODO: implement this for other drivers
417  return 1;
418  }
419 }
420 
421 =head2 requires_write_access
422 
423  Example : $dbc->requires_write_access();
424  Description : See Exceptions
425  Returntype : none
426  Exceptions : Throws if the current user hasn't write access to the database
427  Caller : general
428  Status : Stable
429 
430 =cut
431 
432 sub requires_write_access {
433  my $self = shift;
434  unless ($self->has_write_access) {
435  die sprintf("It appears that %s doesn't have INSERT/UPDATE/DELETE privileges on this database (%s). Please check the credentials\n", $self->username, $self->dbname);
436  }
437 }
438 
439 
440 =head2 _interval_seconds_sql
441 
442  Argument[1] : String. Name of the column for the start of the interval
443  Argument[2] : String, optional. Name of the column for the end of the interval
444  Example : $self->dbc->_interval_seconds_sql();
445  Description : Returns an SQL expression to compute the number of seconds betwen both columns.
446  If the second column name is missing, compute the number of seconds until now instead.
447  Returntype : String
448  Exceptions : none
449  Caller : general
450  Status : Stable
451 
452 =cut
453 
454 sub _interval_seconds_sql {
455  my ($self, $column_from, $column_to) = @_;
456 
457  my $driver = $self->driver();
458 
459  $column_to ||= {
460  'mysql' => '',
461  'sqlite' => q{'now'},
462  'pgsql' => 'CURRENT_TIMESTAMP',
463  }->{$driver};
464 
465  return {
466  'mysql' => "UNIX_TIMESTAMP($column_to)-UNIX_TIMESTAMP($column_from)",
467  'sqlite' => "strftime('%s',$column_to)-strftime('%s',$column_from)",
468  'pgsql' => "EXTRACT(EPOCH FROM $column_to - $column_from)",
469  }->{$driver};
470 }
471 
472 
473 1;
474 
Bio::EnsEMBL::Hive::Utils
Definition: Collection.pm:4
Bio::EnsEMBL::Hive::Utils::URL
Definition: URL.pm:11
Bio::EnsEMBL::Hive::Utils::URL::parse
public parse()
map
public map()
Bio::EnsEMBL::Hive::Utils::SQLErrorParser::is_deadlock
public is_deadlock()
Bio::EnsEMBL::Hive::Utils::SQLErrorParser
Definition: SQLErrorParser.pm:12
Bio::EnsEMBL::Hive::Utils::URL::hash_to_url
public A hash_to_url()
Bio::EnsEMBL::Hive::DBSQL::DBConnection
Definition: DBConnection.pm:20
Bio::EnsEMBL::Hive::Version
Definition: Version.pm:19
Bio::EnsEMBL::Hive::DBSQL::CoreDBConnection
Definition: CoreDBConnection.pm:41
Bio::EnsEMBL::Hive
Definition: Hive.pm:38
Bio::EnsEMBL::Hive::DBSQL::DBConnection::url
public String url()