Friday, January 14, 2011

p_drop_other_connections Revisited

You may have seen a little stored procedure in the newsgroups or out on the web called "p_drop_other_connections" which issues DROP CONNECTION commands for every connection except the one that called the procedure. It's useful when you want to grab complete control of a running SQL Anywhere server and do something important, like ALTER TABLE or CREATE INDEX on the Table From Hell that everyone's using:

/* How to get full control over a SQL Anywhere server...

(1) CALL sa_server_option ( 'ConnsDisabled', 'ON' );

At this point, any attempt to open a new connection will
get "Connections to database have been disabled".
However, current connections will still work until...

(2) CALL p_drop_other_connections();

(3) Do whatever you need to do.

(4) CALL sa_server_option ( 'ConnsDisabled', 'OFF' );

*/
Well, it turns out that the original p_drop_other_connections doesn't work at all on connections to other databases running on the same server: the DROP CONNECTION command gets SQLCode -121 Permission denied.

Here's a new version of p_drop_other_connections that does work, by sending DROP CONNECTION commands to the other databases where they will work without getting permission denied:
1
<br>CREATE PROCEDURE p_drop_other_connections ()<br>BEGIN<br><br>/* How to get full control over a SQL Anywhere server...<br><br>(1) CALL sa_server_option ( 'ConnsDisabled', 'ON' );<br><br>    At this point, any attempt to open a new connection will<br>    get "Connections to database have been disabled". <br>    However, current connections will still work until...<br><br>(2) CALL p_drop_other_connections(); <br><br>(3) Do whatever you need to do.<br><br>(4) CALL sa_server_option ( 'ConnsDisabled', 'OFF' );<br><br>*/<br><br>DECLARE @this_database_number     INTEGER;<br>DECLARE @this_connection_number   INTEGER;<br>DECLARE @database_name            VARCHAR ( 128 );<br>DECLARE @sql                      LONG VARCHAR;<br>DECLARE @sqlcode                  INTEGER;<br>DECLARE @sqlstate                 VARCHAR ( 5 );<br>DECLARE @errormsg                 VARCHAR ( 32767 );<br>DECLARE @connection_count         INTEGER;<br>DECLARE @dropped_count            INTEGER;<br><br>SET @this_database_number   = CONNECTION_PROPERTY ( 'DBNumber' );<br>SET @this_connection_number = CONNECTION_PROPERTY ( 'Number' );<br>SET @connection_count       = 0;<br>SET @dropped_count          = 0;<br><br>SET @database_name = ''; -- until filled in with an actual value<br><br>-- Create a remote server without the DBN filled in.<br><br>BEGIN<br>   DROP SERVER other_database;<br>   EXCEPTION WHEN OTHERS THEN -- ignore any exception<br>END;<br><br>CREATE SERVER other_database CLASS 'SAODBC' <br>   USING 'DRIVER=SQL Anywhere 11;DBN=unknown';<br><br>-- Loop through each connection on the server.<br><br>FOR f_fetch AS c_fetch INSENSITIVE CURSOR FOR<br>SELECT sa_conn_info.DBNumber AS @database_number, <br>       sa_conn_info.Number   AS @connection_number <br>  FROM sa_conn_info() <br> ORDER BY sa_conn_info.DBNumber,<br>       sa_conn_info.Number<br>FOR READ ONLY<br>DO<br><br>   SET @connection_count = @connection_count + 1;<br><br>   CASE<br><br>      WHEN @database_number   = @this_database_number<br>       AND @connection_number = @this_connection_number THEN<br><br>         -- Don't drop this connection.<br><br>      WHEN @database_number = @this_database_number THEN<br><br>         -- Drop local connection.<br><br>         BEGIN<br>            SET @sql = STRING ( <br>               'DROP CONNECTION ', <br>               @connection_number );<br>            MESSAGE @sql TO CLIENT;<br>            EXECUTE IMMEDIATE @sql;<br>            SET @dropped_count = @dropped_count + 1;<br>            EXCEPTION WHEN OTHERS THEN<br>               SELECT SQLCODE, SQLSTATE, ERRORMSG() <br>                 INTO @sqlcode, @sqlstate, @errormsg;<br>               MESSAGE STRING ( 'EXCEPTION: ', <br>                  @sqlcode, ', ', @sqlstate, ', ', @errormsg ) TO CLIENT;<br>         END;<br><br>      ELSE <br><br>         IF DB_PROPERTY ( 'Name', @database_number ) <> @database_name THEN<br><br>            -- Change the DBN for the remote server.<br><br>            SET @database_name = DB_PROPERTY ( 'Name', @database_number );<br><br>            BEGIN<br>               SET @sql = 'ALTER SERVER other_database CONNECTION CLOSE';<br>               MESSAGE @sql TO CLIENT;<br>               EXECUTE IMMEDIATE @sql;<br>               SET @sql = STRING ( <br>                  'ALTER SERVER other_database USING ''DRIVER=SQL Anywhere 11;DBN=',<br>                  @database_name,<br>                  '''' );<br>               MESSAGE @sql TO CLIENT;<br>               EXECUTE IMMEDIATE @sql;<br>               EXCEPTION WHEN OTHERS THEN<br>                  SELECT SQLCODE, SQLSTATE, ERRORMSG() <br>                    INTO @sqlcode, @sqlstate, @errormsg;<br>                  MESSAGE STRING ( 'EXCEPTION: ', <br>                     @sqlcode, ', ', @sqlstate, ', ', @errormsg ) TO CLIENT;<br>            END;<br><br>         END IF;<br><br>         -- Drop the remote connection.<br><br>         BEGIN<br>            SET @sql = STRING ( <br>               'FORWARD TO other_database ''DROP CONNECTION ',<br>               @connection_number,<br>               '''' );<br>            MESSAGE @sql TO CLIENT;<br>            EXECUTE IMMEDIATE @sql;<br>            SET @dropped_count = @dropped_count + 1;<br>            EXCEPTION WHEN OTHERS THEN<br>               SELECT SQLCODE, SQLSTATE, ERRORMSG() <br>                 INTO @sqlcode, @sqlstate, @errormsg;<br>               MESSAGE STRING ( 'EXCEPTION: ', <br>                  @sqlcode, ', ', @sqlstate, ', ', @errormsg ) TO CLIENT;<br>         END;<br>   <br>   END CASE;<br><br>END FOR;<br><br>BEGIN<br>   DROP SERVER other_database;<br>   EXCEPTION WHEN OTHERS THEN -- ignore any exception<br>END;<br><br>MESSAGE STRING ( @dropped_count, ' of ', @connection_count, ' connections dropped.' ) TO CLIENT;<br><br>END;
Here's what a test looks like:
CALL p_drop_other_connections();

ALTER SERVER other_database CONNECTION CLOSE
ALTER SERVER other_database USING 'DRIVER=SQL Anywhere 11;DBN=ddd11b'
FORWARD TO other_database 'DROP CONNECTION 26'
FORWARD TO other_database 'DROP CONNECTION 27'
DROP CONNECTION 30
ALTER SERVER other_database CONNECTION CLOSE
ALTER SERVER other_database USING 'DRIVER=SQL Anywhere 11;DBN=ddd11c'
FORWARD TO other_database 'DROP CONNECTION 28'
FORWARD TO other_database 'DROP CONNECTION 29'
5 of 6 connections dropped.
Here's the before-and-after...



Here are some notes on the code...

The CREATE SERVER statement at line 44 establishes the partial definition for a remote server that will be completed via ALTER SERVER statements later on. The CREATE SERVER statement doesn't actually DO anything, like connect to a remote server, but that's not important here... what's important is that CREATE SERVER doesn't have to be semantically perfect when it's executed.

The FOR statement at line 49 calls the sa_conn_info() procedure to get information about every single connection to every single database on the server.

The CASE statement starting at line 60 makes a three-way choice:
  • This connection? Don't do anything.

  • A different connection to this database? Issue a simple DROP CONNECTION.

  • A different database? Send a DROP CONNECTION to that database via FORWARD TO.
The BEGIN END blocks starting at lines 71, 93 and 114 are there to capture any exceptions and carry on with the next connection.

The EXECUTE IMMEDIATE statement at line 76 drops the local connection. If it gets an exception, the SET @dropped_count = @dropped_count + 1 at line 77 doesn't get executed; instead, the MESSAGE statement at line 81 displays the exception in the ISQL Messages tab.

The ALTER SERVER on lines 94 through 96 forces the remote server to disconnect from the previous database. This is necessary for the change made by the second ALTER SERVER on lines 97 through 102 to have any effect; otherwise, the old connection would remain in effect even though the USING clause has changed.

The FORWARD TO statement on lines 115 through 120 send the DROP CONNECTION statement over to the other database, where it's legal.

No comments: