4

I'm trying to create a SQL CLR stored procedure that will create a table, pass the table name onto a service which will bulk insert some data into it, display the results of the table, then clean up the table.

What I've tried so far:

  1. Use SqlTransaction. Cancelling the transaction works, but it puts my query window into a state where I couldn't continue working on it.

    The transaction active in this session has been committed or aborted by another session

  2. Use TransactionScope. Same issue as 1.

  3. Manually clean up the table in a finally clause by issuing a DROP TABLE SqlCommand. This doesn't seem to get run, though my SqlContext.Pipe.Send() prior to issuing the command does. It doesn't seem like it's related to any time constraints since if I issue a Thread.Sleep(2000) before printing another line, it still prints the second line whereas the command.ExecuteNonQuery() would stop before printing the second line.

  4. Placing the manual cleanup code into a CER or SafeHandle. This doesn't work as having a CER requires some guarantees, including not allocating additional memory or calling methods that are not decorated with a ReliabilityContract.

Am I missing something obvious here? How do I handle the user cancelling their query?

Edit: There were multiple iterations of the code for each scenario, but the general actions taken are along the lines of the following:

    [SqlProcedure]
    public static void GetData(SqlString code)
    {
        Guid guid = Guid.NewGuid();
        using (var connection = new SqlConnection("context connection=true"))
        {
            connection.Open();

            try
            {
                SqlContext.Pipe?.Send("Constrain");

                SqlCommand command1 = new SqlCommand($"CREATE TABLE qb.{code}_{guid:N} (Id INT)", connection);
                command1.ExecuteNonQuery();
                SqlContext.Pipe?.Send($"Create: qb.{code}_{guid:N}");

                //emulate service call
                Thread.Sleep(TimeSpan.FromSeconds(10));

                SqlContext.Pipe?.Send($"Done: qb.{code}_{guid:N}");
            }
            finally
            {
                SqlContext.Pipe?.Send("1");
                //drop table here instead of sleep
                Thread.Sleep(2000);
                SqlContext.Pipe?.Send("2");
            }
        }
    }
8
  • add the relevant code parts Commented Dec 5, 2017 at 16:29
  • a simplistic answer would be if the user cancels the query then immediately drop the table. The service first tests if the table is present before doing the work, if not there it assumes user cancelled the query so it does nothing. Commented Dec 5, 2017 at 16:34
  • @Rob The problem is getting the table to drop after the user cancels the query. The finally block does not execute DROP TABLE as mentioned in attempt 3. Commented Dec 5, 2017 at 16:36
  • And where is the part with the query window? Commented Dec 5, 2017 at 16:39
  • @SirRufo I did not include it as it is just a single call EXEC clrproc 'parameter' Commented Dec 5, 2017 at 16:43

3 Answers 3

2

Unfortunately SQLCLR does not handle query cancellation very well. However, given the error message, that seems to imply that the cancellation does its own ROLLBACK. Have you tried not using a Transaction within the SQLCLR code but instead handling it from outside? Such as:

  1. BEGIN TRAN;
  2. EXEC SQLCLR_Stored_Procedure;
  3. IF (@@TRANCOUNT > 0) ROLLBACK TRAN;

The workflow noted above would need to be enforced. This can be done rather easily by creating a wrapper T-SQL Stored Procedure that executes those 3 steps and only give EXECUTE permission to the wrapper Stored Procedure. If permissions are then needed for the SQLCLR Stored Procedure, that can be accomplished rather easily using module signing:

  1. Create an Asymmetric Key in the same DB as the SQLCLR Stored Procedure
  2. Create a User from that Asymmetric Key
  3. GRANT that Key-based User EXECUTE permission on the SQLCLR Stored Procedure
  4. Sign the wrapper T-SQL Stored Procedure, using ADD SIGNATURE, with that Asymmetric Key

Those 4 steps allow the T-SQL wrapper proc to execute the SQLCLR proc, while the actual application Login can only execute the T-SQL wrapper proc :-). And, in, the event that the cancellation aborts the execution prior to executing ROLLBACK, the Transaction should be automatically rolled-back when the connection closes.

Also, do you have XACT_ABORT set to ON or OFF? UPDATE: O.P. states that it is set to OFF, and setting to ON did not seem to behave any differently.

Have you tried checking the connection state in the finally block? I am pretty sure that the SqlConnection is Closed upon the cancellation. You could try the following approaches, both in the finally block:

  1. Test for the connection state and if Closed, re-open the SqlConnection and then execute the non-query command.

    UPDATE: O.P. states that the connection is still open. Ok, how about closing it and re-opening it?

    UPDATE 2: O.P. tested and found that the connection could not be re-opened.

  2. Since the context is still available, as proven by your print commands working, use something like SqlContext.Pipe.ExecuteAndSend(new SqlCommand("DROP TABLE..;"));

    UPDATE: O.P. states that this did not work.

OR, since you create a guaranteed unique table name in the code, you can try creating the table as a global temporary table (i.e. prefixed with two pound-signs: ##TableName) which will a) be available to the bulk import process, and b) clean itself up when the connection fully closes. In this approach, you technically wouldn't need to perform any manual clean up.

Of course, when Connection Pooling is enabled, the automatic cleanup happens only after the connection is re-opened and the first command is executed. In order to force an immediate cleanup, you would have to connect to SQL Server with Connection Pooling disabled. Is it possible to use a different Connection String just when this Stored Procedure is to be executed that includes Pooling=false;? Given how this Stored Procedure is being used, it does not seem like you would suffer any noticeable performance degradation from disabling Connection Pooling on just this one specific call. To better understand how Connection Pooling – enabled or disabled – affects the automatic cleanup of temporary objects, please see the blog post I just published that details this very behavior:

Sessions, Temporary Objects, and the Afterlife

This approach is probably the best overall since you probably cannot guarantee either that ROLLBACK would be executed (first approach mentioned) or that a finally clause would be executed (assuming you ever got that to work). In the end, uncommitted Transactions will be rolled-back, but if someone executes this via SSMS and it aborts without the ROLLBACK, then they are still in an open Transaction and might not be aware of it. Also, think about the connection being forcibly closed, or the Session being killed, or the server being shutdown / restarted. In those cases tempdb is your friend, whether by using a global temporary table, or at the very least creating the permanent Table in tempdb so that it is automatically removed the next time that the SQL Server starts (due to tempdb being created new, as a copy of model, upon each start of the SQL Server service).

Sign up to request clarification or add additional context in comments.

16 Comments

1. Tested that just now and the connection state is actually still open. 2. Tried this one as well, and it did not execute either.
My concern with the global temp table is that if someone is working with this proc and cancels it a number of times, it would be keeping all that data within tempdb and using up potentially a large amount of space until they close their tab. This could lead to performance issues with other unrelated queries as well.
Ok, interesting, and good to know. Also, if the connection is still open. I wonder if you can close it and re-open it. BUT, hold on, I just thought of two more things. I will update my answer.
I updated with some additional thoughts and questions. the updates are interspersed so please read the whole thing..
I can't expect all developers that use this proc to surround it with a transaction. XACT_ABORT is OFF, so it shouldn't matter in this case. Tried to close and reopen the connection, but it wouldn't execute the connection.Open(). I'm going to have to look into connection pooling some more since I'm not familiar with how it works.
|
1

Stepping back there are probably much better ways to pass the data out of your CLR procedure.

1) you can simply use the SqlContext Pipe to return a resultset without creating a table.

2) you can create a temp table (#) in the calling code and access it from inside the CLR procedure. You might want to introduce a TSQL wrapper procedure to make this convenient.

Anyway using BEGIN TRAN/COMMIT | ROLLBACK worked for me:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Threading;

static class SqlConnectionExtensions
{
    public static DataTable ExecuteDataTable(this SqlConnection con, string sql, params SqlParameter[] parameters)
    {
        var cmd = new SqlCommand(sql, con);
        foreach (var p in parameters)
        {
            cmd.Parameters.Add(p);
        }
        using (var dr = cmd.ExecuteReader())
        {
            var dt = new DataTable();
            dt.Load(dr);
            return dt;
        }
    }

    public static int ExecuteNonQuery(this SqlConnection con, string sql, params SqlParameter[] parameters)
    {
        var cmd = new SqlCommand(sql, con);
        foreach (var p in parameters)
        {
            cmd.Parameters.Add(p);
        }
        return cmd.ExecuteNonQuery();
    }

}

public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void GetData(SqlString code)
    {
        Guid guid = Guid.NewGuid();
        using (var connection = new SqlConnection("context connection=true"))
        {
            connection.Open();

            try
            {
                connection.ExecuteNonQuery("begin transaction;");
                SqlContext.Pipe?.Send("Constrain");

                connection.ExecuteNonQuery($"CREATE TABLE qb.{code}_{guid:N} (Id INT)");

                SqlContext.Pipe?.Send($"Create: qb.{code}_{guid:N}");

                //emulate service call
                Thread.Sleep(TimeSpan.FromSeconds(10));

                SqlContext.Pipe?.Send($"Done: qb.{code}_{guid:N}");
                connection.ExecuteNonQuery("commit transaction");

            }
            catch (Exception ex)
            {
                connection.ExecuteNonQuery("rollback;");
                throw;
            }

        }

    }
}

8 Comments

#1 won't work due to the specific workflow of the external service needing to load data into that table first. #2 won't work due to the data being loaded by another Session. That is why I suggested using a global temp table instead. #3 would be interesting if the catch block still had the open connection whereas the finally block does not seem to. Though I still prefer to not even rely upon that manual cleanup when a global temp table will be automatically cleaned up no matter what happens.
"external service needing to load data into that table first" The CLR proc could load a temp table and then pipe the contents to the client. "the data being loaded by another Session" the "context connection" is not a different session. It's just an API that allows CLR code to access the session that invoked the CLR stored procedure.
Yes, I am aware of what the Context Connection is. The very beginning of the question states: "pass the table name onto a service which will bulk insert some data into it". You even copied the C# comment of //emulate service call in your code here. That would be a different Session.
@DavidBrowne-Microsoft The connection that the service will have is not going to be a context connection and as far as I'm aware, there's no way to pass this connection from SQL CLR to the service. I am aware that you could return the results without creating a table, but that would put my business logic within the SQL CLR function and I do not want that.
Ahh, got it. In that case you can't use transactions at all, since the external service wouldn't be able to see a table created in an uncommitted transaction.
|
0

While it is not something advised for use, this would be an ideal match for sp_bindsession. With sp_bindsession, you would call sp_getbindtoken from the context session, pass the token to the server and call sp_bindsession from the service connection.

Afterwards, the two connections behave "as one", with temporaries and transactions being transparently propagated.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.