17 Copyright [1999-2015] Wellcome Trust Sanger Institute and the EMBL-European Bioinformatics Institute
18 Copyright [2016-2022] EMBL-European Bioinformatics Institute
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
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.
31 Please subscribe to the Hive mailing list: http:
36 package Bio::EnsEMBL::Hive::DBSQL::DBConnection;
41 use Time::HiRes (
'usleep');
45 use base (
'Bio::EnsEMBL::Hive::DBSQL::CoreDBConnection');
52 if(my $url =
delete $flags{
'-url'}) {
55 foreach my $name (
'driver',
'host',
'port',
'user',
'pass',
'dbname' ) {
58 foreach my $name ( keys %{$parsed_url->{
'conn_params'}} ) {
62 return $class->SUPER::new( %flags );
65 throw(
"Could not create DBC because could not parse the URL '$url'");
68 return $class->SUPER::new( @_ );
73 sub _optional_pair { # helper
function 74 my ($key, $value) = @_;
76 return defined($value) ? ($key => $value) : ();
81 my ($self, $psw_env_var_name) = @_;
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.
'}';
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),
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),
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:
123 my ($self, $psw_env_var_name) = @_;
129 sub connect { # a wrapper that imitates CSMA/CD protocol
's incremental backoff-and-retry approach 136 foreach my $attempt (1..$attempts) { 138 $retval = $self->SUPER::connect( @_ ); 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 ?) 147 warn "Possibly transient problem conecting to the database (attempt #$attempt). Will try again in $sleep_sec sec"; 149 usleep( $sleep_sec*1000000 ); 153 } else { # but definitely report other errors 158 last; # stop looping once we succeeded 162 die "After $attempts attempts still could not connect() : $@"; 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)
171 my $sql_params = shift @_;
172 my $deadlock_log_callback = shift @_;
174 my $sql_cmd = shift @$sql_params;
177 my $sleep_max_sec = 1;
182 foreach my $attempt (1..$attempts) {
184 my $sth = $self->prepare( $sql_cmd );
185 $retval = $sth->execute( @$sql_params );
189 $query_msg =
"QUERY: $sql_cmd, PARAMS: (".join(
', ',@$sql_params).
")";
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
195 my $this_sleep_sec = int( rand( $sleep_max_sec )*100 ) / 100.0;
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" );
201 usleep( $this_sleep_sec*1000000 );
205 }
else { # but definitely report other errors
207 die
"$@ -- $query_msg";
210 last; # stop looping once we succeeded
213 die
"After $attempts attempts the query $query_msg still cannot be run: $@" if($@);
219 our $pass_internal_counter = 0;
221 my ($self, $executable, $prepend, $append, $sqlcmd, $hide_password_in_env) = @_;
223 my $driver = $self->driver ||
'mysql';
225 my $dbname = $self->dbname;
227 if($sqlcmd =~ /(DROP\s+DATABASE(?:\s+IF\s+EXISTS)?\s*?)(?:\s+(\w+))?/i) {
230 if($driver eq
'sqlite') {
231 return [
'rm',
'-f', $dbname];
234 die
"'DROP DATABASE' needs a database name\n";
236 if ($driver eq
'mysql') {
237 $sqlcmd =
"$1 \`$dbname\`" unless $2;
239 $sqlcmd =
"$1 $dbname" unless $2;
243 } elsif($sqlcmd =~ /(CREATE\s+DATABASE(?:\s+IF\s+NOT\s+EXISTS)?\s*?)(?:\s+(\w+))?/i ) {
246 if($driver eq
'sqlite') {
247 return [
'touch', $dbname];
250 die
"'CREATE DATABASE' needs a database name\n";
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";
256 if ($driver eq
'mysql') {
257 $sqlcmd =
"$1 \`$dbname\`" unless $2;
259 $sqlcmd =
"$1 $dbname" unless $2;
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;
276 $hidden_password = $self->password;
280 if($driver eq
'mysql') {
281 $executable ||=
'mysql';
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;
293 } elsif($driver eq
'pgsql') {
294 $executable ||=
'psql';
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;
305 } elsif($driver eq
'sqlite') {
306 $executable ||=
'sqlite3';
308 die
"sqlite requires a database (file) name\n" unless $dbname;
310 push @cmd, $executable;
311 push @cmd, @$prepend
if ($prepend && @$prepend);
313 push @cmd, $sqlcmd
if $sqlcmd;
316 push @cmd, @$append
if ($append && @$append);
322 =head2 run_in_transaction
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() 331 sub run_in_transaction { 332 my ($self, $callback) = @_; 334 # Save the original value of disconnect_when_inactive() 335 my $original_dwi = $self->disconnect_when_inactive(); 336 $self->disconnect_when_inactive(0); 338 $self->reconnect() unless $self->db_handle()->ping(); 340 # Save the original value of "AutoCommit" 341 my $original_ac = $self->db_handle()->{'AutoCommit
'}; 342 $self->db_handle()->{'AutoCommit
'} = 0; 346 $result = $callback->(); 347 # FIXME: does this work if the "MySQL server has gone away" ? 348 $self->db_handle()->commit(); 352 #If there is an error then we apply rollbacks 354 eval { $self->db_handle()->rollback(); }; 357 # Restore the original values 358 $self->db_handle()->{'AutoCommit
'} = $original_ac; 359 $self->disconnect_when_inactive($original_dwi); 361 die "ABORT: Transaction aborted because of error: ${error}" if $error; 366 =head2 has_write_access 368 Example : my $can_do = $dbc->has_write_access(); 369 Description : Tells whether the underlying database connection has write access to the database 377 sub has_write_access { 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); 385 return $has_write_access_from_some_host; 387 # TODO: implement this for other drivers 392 =head2 requires_write_access 394 Example : $dbc->requires_write_access(); 395 Description : See Exceptions 397 Exceptions : Throws if the current user hasn't write access to the database
403 sub requires_write_access {
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);