I have a stored procedure which inserts into a table. This insert is wrapped within a transaction. Also, the calling app wraps the SQlCommand call to this stored procedure in a SQLTransaction. If all is good, the transactions are committed, if not the calling app and the stored procedure will rollback. Upon failure, the app retries the insert by calling the SP again.
Here is a simplified database code:
create table Employee (
[Key] int identity(1,1) not null,
[EmployeeId] varchar(100) not null,
[EmployeeName] varchar(100) not null,
constraint Employee_PK primary key clustered ( [Key] asc),
constraint Employee_Unq_EmployeeId unique( [EmployeeId] )
)
go
create procedure InsertEmployee
@employeeId varchar(100),
@employeeName varchar(100)
as
begin
declare @key int;
begin tran
insert into employee values (@employeeId, @employeeName);
set @key = @@identity;
if (@@error <> 0) GOTO SQLError
commit
select @key as EmployeeKey;
return 0;
SQLError:
rollback
return @@error
end
Here is the simplified version of the calling app:
static void InsertEmployee(string employeeId, string employeeName)
{
bool done = false;
int retryCount = 0;
while(!done)
{
done = CallInsertEmployeeStoredProc(employeeId, employeeName);
if(!done)
{
retryCount++;
if (retryCount >= MAXALLOWEDRETRIES)
throw new ApplicationException("Unable to insert employee");
Thread.Sleep(TimeSpan.FromSeconds(20));
}
}
}
private static bool CallInsertEmployeeStoredProc(string employeeId, string employeeName)
{
using (SqlConnection conn = new SqlConnection(CONNSTRING))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand())
{
using (SqlTransaction tran = conn.BeginTransaction())
{
cmd.Connection = conn;
cmd.CommandText = "InsertEmployee";
cmd.CommandType = CommandType.StoredProcedure;
cmd.Transaction = tran;
var eId = cmd.Parameters.Add("@employeeId", SqlDbType.VarChar);
eId.Value = employeeId;
eId.Direction = ParameterDirection.Input;
var eName = cmd.Parameters.Add("@employeeName", SqlDbType.VarChar);
eName.Value = employeeName;
eName.Direction = ParameterDirection.Input;
try
{
cmd.ExecuteScalar();
tran.Commit();
return true;
}
catch (Exception ex)
{
try
{
tran.Rollback();
}
catch (Exception trex)
{
Console.WriteLine(trex);
}
Console.WriteLine(ex);
return false;
}
}
}
}
}
But, occasionally, the stored procedure times out, and I see this initial set of errors:
InvalidOperationException This SqlTransaction has completed; it is no longer usable.
at [ at System.Data.SqlClient.SqlTransaction.ZombieCheck()
at System.Data.SqlClient.SqlTransaction.Rollback()
at MyTestApp.Program.CallInsertEmployeeStoredProc(string employeeId, string employeeName)]
SqlException Timeout expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
Win32Exception The wait operation timed out
at [ MyTestApp.Program.CallInsertEmployeeStoredProc(string employeeId, string employeeName)]
But, when the app retires to insert, thats when it gets a Unique Key violation error since that Employee Insert was already committed by the Stored Procedure:
System.Data.SqlClient.SqlException (0x80131904): Violation of UNIQUE KEY constraint 'Employee_Unq_EmployeeId'. Cannot insert duplicate key in object 'dbo.Employee'. The duplicate key value is(Emp002).
The statement has been terminated.
at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at System.Data.SqlClient.SqlDataReader.TryConsumeMetaData()
at System.Data.SqlClient.SqlDataReader.get_MetaData()
at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString, Boolean isInternal, Boolean forDescribeParameterEncryption, Boolean shouldCacheForAlwaysEncrypted)
at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, Boolean inRetry, SqlDataReader ds, Boolean describeParameterEncryptionRequest)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean& usedCache, Boolean asyncWrite, Boolean inRetry)
at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method)
at System.Data.SqlClient.SqlCommand.ExecuteScalar()
at MyTestApp.Program.CallInsertEmployeeStoredProc(String employeeId, String employeeName)
ClientConnectionId:254fe674-9339-45a7-bb79-e3305bc3f850
Error Number:2627,State:1,Class:14
How can this ever happen? How can SQLServer commit when the calling app tried to rollback and failed?
User contributions licensed under CC BY-SA 3.0