ensembl-hive  2.5
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-2022] 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');
44 
45 use base ('Bio::EnsEMBL::Hive::DBSQL::CoreDBConnection');
46 
47 
48 sub new {
49  my $class = shift;
50  my %flags = @_;
51 
52  if(my $url = delete $flags{'-url'}) {
53  if(my $parsed_url = Bio::EnsEMBL::Hive::Utils::URL::parse( $url )) {
54 
55  foreach my $name ( 'driver', 'host', 'port', 'user', 'pass', 'dbname' ) {
56  $flags{ "-$name" } //= $parsed_url->{$name};
57  }
58  foreach my $name ( keys %{$parsed_url->{'conn_params'}} ) {
59  $flags{ "-$name" } //= $parsed_url->{'conn_params'}->{$name};
60  }
61 
62  return $class->SUPER::new( %flags );
63 
64  } else {
65  throw("Could not create DBC because could not parse the URL '$url'");
66  }
67  } else {
68  return $class->SUPER::new( @_ );
69  }
70 }
71 
72 
73 sub _optional_pair { # helper function
74  my ($key, $value) = @_;
75 
76  return defined($value) ? ($key => $value) : ();
77 }
78 
79 
80 sub to_url_hash {
81  my ($self, $psw_env_var_name) = @_;
82 
83  my $psw_expression;
84  if($psw_expression = $self->password) {
85  if($psw_env_var_name) {
86  $ENV{$psw_env_var_name} = $psw_expression;
87  $psw_expression = '${'.$psw_env_var_name.'}';
88  }
89  }
90 
91  my $url_hash = {
92  _optional_pair('driver', $self->driver),
93  _optional_pair('user', $self->username),
94  _optional_pair('pass', $psw_expression),
95  _optional_pair('host', $self->host),
96  _optional_pair('port', $self->port),
97  _optional_pair('dbname', $self->dbname),
98 
99  'conn_params' => {
100  _optional_pair('disconnect_when_inactive', $self->disconnect_when_inactive),
101  _optional_pair('wait_timeout', $self->wait_timeout),
102  _optional_pair('reconnect_when_lost', $self->reconnect_when_lost),
103  },
104  };
105 
106  return $url_hash;
107 }
108 
109 
110 =head2 url
111 
112  Arg [1] : String $environment_variable_name_to_store_password_in (optional)
113  Example : $url = $dbc->url;
114  Description: Constructs a URL string for this database connection.
115  Returntype : string of format mysql://<user>:<pass>@<host>:<port>/<dbname>
116  or sqlite:
117  Exceptions : none
118  Caller : general
119 
120 =cut
121 
122 sub url {
123  my ($self, $psw_env_var_name) = @_;
124 
125  return Bio::EnsEMBL::Hive::Utils::URL::hash_to_url( $self->to_url_hash( $psw_env_var_name ) );
126 }
127 
128 
129 sub connect { # a wrapper that imitates CSMA/CD protocol's incremental backoff-and-retry approach
130  my $self = shift @_;
131 
132  my $attempts = 9;
133  my $sleep_sec = 30;
134  my $retval;
135 
136  foreach my $attempt (1..$attempts) {
137  eval {
138  $retval = $self->SUPER::connect( @_ );
139  1;
140  } or do {
141  if( ($@ =~ /Could not connect to database.+?failed: Too many connections/s) # problem on server side (configured with not enough connections)
142  or ($@ =~ /Could not connect to database.+?failed: Can't connect to \w+? server on '.+?' \(99\)/s) # problem on client side (cooling down period after a disconnect)
143  or ($@ =~ /Could not connect to database.+?failed: Can't connect to \w+? server on '.+?' \(110\)/s) # problem on server side ("Connection timed out"L the server is temporarily dropping connections until it reaches a reasonable load)
144  or ($@ =~ /Could not connect to database.+?failed: Lost connection to MySQL server at 'reading authorization packet', system error: 0/s) # problem on server side (server too busy ?)
145  ) {
146 
147  warn "Possibly transient problem conecting to the database (attempt #$attempt). Will try again in $sleep_sec sec";
148 
149  usleep( $sleep_sec*1000000 );
150  $sleep_sec *= 2;
151  next;
152 
153  } else { # but definitely report other errors
154 
155  die $@;
156  }
157  };
158  last; # stop looping once we succeeded
159  }
160 
161  if($@) {
162  die "After $attempts attempts still could not connect() : $@";
163  }
164 
165  return $retval;
166 }
167 
168 
169 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)
170  my $self = shift @_;
171  my $sql_params = shift @_;
172  my $deadlock_log_callback = shift @_;
173 
174  my $sql_cmd = shift @$sql_params;
175 
176  my $attempts = 9;
177  my $sleep_max_sec = 1;
178 
179  my $retval;
180  my $query_msg;
181 
182  foreach my $attempt (1..$attempts) {
183  eval {
184  my $sth = $self->prepare( $sql_cmd );
185  $retval = $sth->execute( @$sql_params );
186  $sth->finish;
187  1;
188  } or do {
189  $query_msg = "QUERY: $sql_cmd, PARAMS: (".join(', ',@$sql_params).")";
190 
191  if( ($@ =~ /Deadlock found when trying to get lock; try restarting transaction/) # MySQL error
192  or ($@ =~ /Lock wait timeout exceeded; try restarting transaction/) # MySQL error
193  ) {
194 
195  my $this_sleep_sec = int( rand( $sleep_max_sec )*100 ) / 100.0;
196 
197  if( $deadlock_log_callback ) {
198  $deadlock_log_callback->( " temporarily failed due to a DEADLOCK in the database (attempt #$attempt). Will try again in $this_sleep_sec sec" );
199  }
200 
201  usleep( $this_sleep_sec*1000000 );
202  $sleep_max_sec *= 2;
203  next;
204 
205  } else { # but definitely report other errors
206 
207  die "$@ -- $query_msg";
208  }
209  };
210  last; # stop looping once we succeeded
211  }
212 
213  die "After $attempts attempts the query $query_msg still cannot be run: $@" if($@);
214 
215  return $retval;
216 }
217 
218 
219 our $pass_internal_counter = 0;
220 sub to_cmd {
221  my ($self, $executable, $prepend, $append, $sqlcmd, $hide_password_in_env) = @_;
222 
223  my $driver = $self->driver || 'mysql';
224 
225  my $dbname = $self->dbname;
226  if($sqlcmd) {
227  if($sqlcmd =~ /(DROP\s+DATABASE(?:\s+IF\s+EXISTS)?\s*?)(?:\s+(\w+))?/i) {
228  $dbname = $2 if $2;
229 
230  if($driver eq 'sqlite') {
231  return ['rm', '-f', $dbname];
232  } else {
233  if (not $dbname) {
234  die "'DROP DATABASE' needs a database name\n";
235  }
236  if ($driver eq 'mysql') {
237  $sqlcmd = "$1 \`$dbname\`" unless $2;
238  } else {
239  $sqlcmd = "$1 $dbname" unless $2;
240  }
241  $dbname = '';
242  }
243  } elsif($sqlcmd =~ /(CREATE\s+DATABASE(?:\s+IF\s+NOT\s+EXISTS)?\s*?)(?:\s+(\w+))?/i ) {
244  $dbname = $2 if $2;
245 
246  if($driver eq 'sqlite') {
247  return ['touch', $dbname];
248  } else {
249  if (not $dbname) {
250  die "'CREATE DATABASE' needs a database name\n";
251  }
252  my %limits = ( 'mysql' => 64, 'pgsql' => 63 );
253  if (length($dbname) > $limits{$driver}) {
254  die "Database name '$dbname' is too long (> $limits{$driver}). Cannot create the database\n";
255  }
256  if ($driver eq 'mysql') {
257  $sqlcmd = "$1 \`$dbname\`" unless $2;
258  } else {
259  $sqlcmd = "$1 $dbname" unless $2;
260  }
261  $dbname = '';
262  }
263  }
264  }
265 
266  my @cmd;
267 
268  my $hidden_password;
269  if ($self->password) {
270  if ($hide_password_in_env) {
271  my $pass_variable = "EHIVE_TMP_PASSWORD_${pass_internal_counter}";
272  $pass_internal_counter++;
273  $ENV{$pass_variable} = $self->password;
274  $hidden_password = '$'.$pass_variable;
275  } else {
276  $hidden_password = $self->password;
277  }
278  }
279 
280  if($driver eq 'mysql') {
281  $executable ||= 'mysql';
282 
283  push @cmd, ('env', 'MYSQL_PWD='.$hidden_password) if ($self->password);
284  push @cmd, $executable;
285  push @cmd, @$prepend if ($prepend && @$prepend);
286  push @cmd, '--host='.$self->host if $self->host;
287  push @cmd, '--port='.$self->port if $self->port;
288  push @cmd, '--user='.$self->username if $self->username;
289 # push @cmd, '--password='.$hidden_password if $self->password;
290  push @cmd, ('-e', $sqlcmd) if $sqlcmd;
291  push @cmd, $dbname if $dbname;
292 
293  } elsif($driver eq 'pgsql') {
294  $executable ||= 'psql';
295 
296  push @cmd, ('env', 'PGPASSWORD='.$hidden_password) if ($self->password);
297  push @cmd, $executable;
298  push @cmd, @$prepend if ($prepend && @$prepend);
299  push @cmd, ('-h', $self->host) if defined($self->host);
300  push @cmd, ('-p', $self->port) if defined($self->port);
301  push @cmd, ('-U', $self->username) if defined($self->username);
302  push @cmd, ('-c', $sqlcmd) if $sqlcmd;
303  push @cmd, $dbname if $dbname;
304 
305  } elsif($driver eq 'sqlite') {
306  $executable ||= 'sqlite3';
307 
308  die "sqlite requires a database (file) name\n" unless $dbname;
309 
310  push @cmd, $executable;
311  push @cmd, @$prepend if ($prepend && @$prepend);
312  push @cmd, $dbname;
313  push @cmd, $sqlcmd if $sqlcmd;
314  }
315 
316  push @cmd, @$append if ($append && @$append);
317 
318  return \@cmd;
319 }
320 
321 
322 =head2 run_in_transaction
323 
324  Description : Wrapper that first sets AutoCommit to 0, runs some user code, and at the end issues a commit() / rollback()
325  It also has to temporarily set disconnect_when_inactive() to 1 because a value of 0 would cause the
326  DBConnection object to disconnect early, which would rollback the transaction.
327  NB: This is essentially a trimmed copy of Ensembl's Utils::SqlHelper::transaction()
328 
329 =cut
330 
331 sub run_in_transaction {
332  my ($self, $callback) = @_;
333 
334  # Save the original value of disconnect_when_inactive()
335  my $original_dwi = $self->disconnect_when_inactive();
336  $self->disconnect_when_inactive(0);
337 
338  $self->reconnect() unless $self->db_handle()->ping();
339 
340  # Save the original value of "AutoCommit"
341  my $original_ac = $self->db_handle()->{'AutoCommit'};
342  $self->db_handle()->{'AutoCommit'} = 0;
343 
344  my $result;
345  eval {
346  $result = $callback->();
347  # FIXME: does this work if the "MySQL server has gone away" ?
348  $self->db_handle()->commit();
349  };
350  my $error = $@;
351 
352  #If there is an error then we apply rollbacks
353  if($error) {
354  eval { $self->db_handle()->rollback(); };
355  }
356 
357  # Restore the original values
358  $self->db_handle()->{'AutoCommit'} = $original_ac;
359  $self->disconnect_when_inactive($original_dwi);
360 
361  die "ABORT: Transaction aborted because of error: ${error}" if $error;
362  return $result;
363 }
364 
365 
366 =head2 has_write_access
367 
368  Example : my $can_do = $dbc->has_write_access();
369  Description : Tells whether the underlying database connection has write access to the database
370  Returntype : Boolean
371  Exceptions : none
372  Caller : general
373  Status : Stable
374 
375 =cut
376 
377 sub has_write_access {
378  my $self = shift;
379  if ($self->driver eq 'mysql') {
380  my $user_entries = $self->selectall_arrayref('SELECT Insert_priv, Update_priv, Delete_priv FROM mysql.user WHERE user = ?', undef, $self->username);
381  my $has_write_access_from_some_host = 0;
382  foreach my $entry (@$user_entries) {
383  $has_write_access_from_some_host ||= !scalar(grep {$_ eq 'N'} @$entry);
384  }
385  return $has_write_access_from_some_host;
386  } else {
387  # TODO: implement this for other drivers
388  return 1;
389  }
390 }
391 
392 =head2 requires_write_access
393 
394  Example : $dbc->requires_write_access();
395  Description : See Exceptions
396  Returntype : none
397  Exceptions : Throws if the current user hasn't write access to the database
398  Caller : general
399  Status : Stable
400 
401 =cut
402 
403 sub requires_write_access {
404  my $self = shift;
405  unless ($self->has_write_access) {
406  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);
407  }
408 }
409 
410 
411 1;
412