Rafael
Rafael

Reputation: 3111

Stored procedure with cursor is permacycling

This is the stored procedure for any reason this perma-looping

I have to make some aclarations

its the first time that i use SCROLL KEYSET SCROLL_LOCKS and WHERE CURRENT OF

the short explanation of what this does: replacing a temporal id for many rows with a permanent id that is the MAX(num_subproceso)+1 but I want it to do in ACID fashion to solve possible concurrence iusses.

I can't use transaction directly in my app, the reason for this is that I'm using a classes set of my authorship that abstract the structure of the database and the data behavior in this way, only of the downside of my specific implementation is that i can't use a transaction because my code often in rapid opening/closing connection cycles and i need to make a transaction of big chunk of sqls.

To avoid this I finished generating a negative random id to write the rows in the database and later use a stored proc to use a transaction and use a cursor with row lock to avoid (maybe I'm wrong here) that other user get the same id using MAX(...)+1 and occurs a problem where 2 user write the same primary key.

Why I'm saying that I'm preventing concurrency problem (I could be wrong): if I lock a row that I'm using and other user makes an operation (ex. MAX(...) to get a new id) over the whole set of rows in the same table, the lock that is blocking my row prevent that the 2nd client can get the result and stay in wait state until my lock is released

The process is long and get various steps in a way that pass a time between start and finish the whole process .

... the permanent id is the max number of that type of id plus one ... in this way; while the temporal id is the same at first the permanent id can vary

data example

(before)

cve_subproceso (PK) | num_subproceso (PK) | inidate (dd-mm-yyyy hh:MM:ss)
--------------------+---------------------+---------------------
A01                 | -27813578125        | 01-05-2011 09:00:00
A02                 | -27813578125        | 15-05-2011 10:00:00
A03                 | -27813578125        | 16-05-2011 07:30:00
A07                 | -27813578125        | 21-05-2011 09:15:00
A12                 | -27813578125        | 30-05-2011 10:00:00
...                 | ...                 | ...

(after)

cve_subproceso (PK) | num_subproceso (PK) | inidate (dd-mm-yyyy hh:MM:ss)
--------------------+---------------------+---------------------
A01                 | 157                 | 01-05-2011 09:00:00
A02                 | 122                 | 15-05-2011 10:00:00
A03                 | 15                  | 16-05-2011 07:30:00
A07                 | 90                  | 21-05-2011 09:15:00
A12                 | 140                 | 30-05-2011 10:00:00
...                 | ...                 | ...

How is calculated [num_subproceso]

the number of the permanent id is based in how many [cve_subproceso] are in the table.

related to this example ... exists or existed at given time 156 'A01' secuenced from 1 to 156 when i'm adding 157 to A01 in this case, is because i calculated MAX([num_subproceso]) and MAX() returned 156

cve_subproceso (PK) | num_subproceso (PK) | inidate (dd-mm-yyyy hh:MM:ss)
--------------------+---------------------+---------------------
A01                 | 156                 | 01-05-2011 09:00:00
A01                 | 155                 | 15-04-2011 10:00:00
A01                 | 154                 | 03-04-2011 07:30:00
A01                 | 152                 | 11-03-2011 09:15:00
A01                 | 151                 | 10-01-2011 10:00:00
...                 | ...                 | ...

i repeat the same operation for each value of [cve_subproceso] ...

SP code

ALTER PROCEDURE [dbo].[ReprocesarEventos] 
-- Add the parameters for the stored procedure here
@numSubproceso int = 0
AS 
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;
    BEGIN TRANSACTION;
    -- Insert statements for procedure here
    DECLARE @nuevoID integer;
    DECLARE @cveSubProc varchar(4);
    DECLARE cUpd CURSOR SCROLL KEYSET SCROLL_LOCKS
        FOR SELECT [cve_subproceso] FROM [dbo].[calendario] WHERE [num_subproceso]=     @numSubproceso
    FOR UPDATE OF [num_subproceso];
OPEN cUpd;
FETCH NEXT FROM cUpd INTO @cveSubProc;
WHILE (@@FETCH_STATUS <> -1)
BEGIN
    IF (@@FETCH_STATUS <> -2)
    BEGIN
        SELECT @nuevoID=(ISNULL(MAX([num_subproceso]),0)+1) FROM [dbo].[calendario] WHERE [cve_subproceso]=RTRIM(LTRIM(@cveSubProc));
        IF(@nuevoID < 0)
        BEGIN
            SET @nuevoID = 1;
        END;
        UPDATE [dbo].[calendario] SET [num_subproceso]=@nuevoID WHERE CURRENT OF cUpd;
    END;
    FETCH NEXT FROM cUpd INTO @cveSubProc;
END;
CLOSE cUpd;
DEALLOCATE cUpd;

IF(@@ERROR <> 0)
 BEGIN
    -- Rollback the transaction
    ROLLBACK TRANSACTION;

    -- Raise an error and return
    RAISERROR ('Oops! - something went wrong.', 16, 1);
    RETURN;
 END;
COMMIT TRANSACTION;
END;

what is wrong here?

Upvotes: 0

Views: 483

Answers (1)

Martin Smith
Martin Smith

Reputation: 453287

I think this should do what you need. You need SERIALIABLE to lock the range above MAX. This will cause blocking but that's the point!

Demo Data

CREATE TABLE [dbo].[calendario](
    [cve_subproceso] [char](3) NULL,
    [num_subproceso] [bigint] NULL
)

INSERT INTO [dbo].[calendario] 
SELECT 'A01',-27813578125 UNION ALL
SELECT 'A02',-27813578125 UNION ALL
SELECT 'A03',-27813578125 UNION ALL
SELECT 'A07',-27813578125 UNION ALL
SELECT 'A12',-27813578125 UNION ALL
SELECT 'A01',156 UNION ALL          /*Add a few rows of pre-existing data*/
SELECT 'A01',157 UNION ALL
SELECT 'A02',121

The Query

DECLARE @numSubproceso BIGINT = -27813578125

/*Run the query*/
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

WITH t1
     AS (SELECT *,
                MAX(num_subproceso) OVER (PARTITION BY cve_subproceso) AS
                   max_num_subproceso
         FROM   [dbo].[calendario]),
     t2
     AS (SELECT *,
                CASE
                  WHEN max_num_subproceso < 0 THEN 0
                  ELSE max_num_subproceso
                END + Row_number() OVER (PARTITION BY cve_subproceso ORDER BY
                      cve_subproceso) AS
                new_num_subproceso
         FROM   t1
         WHERE  [num_subproceso] = @numSubproceso)
UPDATE t2
SET    num_subproceso = new_num_subproceso  

Result afterwards

cve_subproceso num_subproceso
-------------- --------------------
A01            158
A02            122
A03            1
A07            1
A12            1
A01            156
A01            157
A02            121

Upvotes: 1

Related Questions