Bug #73878 GET_LOCK() in a UDF is not sequencing properly
Submitted: 10 Sep 2014 15:22 Modified: 19 Jul 2018 14:30
Reporter: Van Stokes Email Updates:
Status: No Feedback Impact on me:
None 
Category:MySQL Server Severity:S2 (Serious)
Version:5.6.20 x64 OS:Linux (Ubuntu 14.04.1 x64)
Assigned to: CPU Architecture:Any
Tags: GET_LOCK

[10 Sep 2014 15:22] Van Stokes
Description:
GET_LOCK() in a UDF is not sequencing properly causing the function to work incorrectly. The UDF creates a new sequence record and new records are being duplicated.

I have provided the table configuration, UFD, and a shell script to simulate the problem. If you run the shell script you will find that the column 'file_number' will get a duplicate entry - but column 'id' will not be duplicated.

A duplicate entry of column 'file_number' should not happen. GET_LOCK() is called to prevent client collisions to this function yet the bash script will demonstrate that a duplication does occur leading to the conclusion there is a sequencing issue somewhere. It appears that the INSERT isn't being committed prior to the next client getting access to the lock and attempting to generate the next sequence.

Tested on:
Ubuntu 14.04.1 LTS x64
MySQL Server 5.6.20 x64 and 5.5.38 x64

How to repeat:
CREATE TABLE `lcl_file_number` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `file_number` varchar(50) NOT NULL,
  `created_datetime` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `lcl_file_number_idx1` (`file_number`),
  KEY `lcl_file_number_idx2` (`created_datetime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
;

-- #######
-- insert initial starting record
INSERT INTO lcl_file_number ( file_number, created_datetime ) VALUES( 'A00000',NOW());
-- #######

DELIMITER $$
DROP FUNCTION IF EXISTS `LCLFileNumberCreate`$$
CREATE DEFINER=`root`@`%` FUNCTION `LCLFileNumberCreate`() RETURNS BIGINT(20) UNSIGNED
MODIFIES SQL DATA
MAIN: BEGIN
  DECLARE mLockName VARCHAR(50) DEFAULT NULL;
  DECLARE mLock BOOL DEFAULT FALSE;
  DECLARE mFileNumberID BIGINT(20) UNSIGNED DEFAULT NULL;
  DECLARE mFileNumber VARCHAR(50) DEFAULT NULL;
  DECLARE mLetter VARCHAR(5) DEFAULT "";
  DECLARE mLetterAsInt TINYINT UNSIGNED DEFAULT 0;
  DECLARE mNumber INT(11) UNSIGNED DEFAULT 0;
  
  SET mLockName = CONCAT( DATABASE() , ".LCLFileNumber" );
  SET mLock = GET_LOCK( mLockName, 30 );
  IF ISNULL( mLock) OR mLock = 0 THEN
	  RETURN NULL;
  END IF;
  
  SELECT
  	id, file_number
  INTO
  	mFileNumberID, mFileNumber
  FROM lcl_file_number
  WHERE SUBSTR( file_number, 1, 1 ) BETWEEN 'A' AND 'Z'
  AND SUBSTR( file_number, 1, 2 ) != 'ZZ'
  ORDER BY id DESC
  LIMIT 1;
  
  IF ISNULL( mFileNumber ) OR mFileNumberID < 1 THEN
		SET mLock = RELEASE_LOCK( mLockName );
		RETURN NULL;
  END IF;
  
  SET mLetter = SUBSTR( mFileNumber, 1, 1 );
  SET mNumber = SUBSTR( mFileNumber, 2 );
  
  IF mNumber < 99999 THEN
	  SET mNumber = mNumber + 1;
  ELSE
		SET mLetterAsInt = ASCII( mLetter );
		CASE mLetter
		  WHEN 'H' THEN SET mLetterAsInt = mLetterAsInt + 2;
		  WHEN 'K' THEN SET mLetterAsInt = mLetterAsInt + 2;
		  WHEN 'N' THEN SET mLetterAsInt = mLetterAsInt + 2;
		  WHEN 'Z' THEN SET mLetterAsInt = ASCII( 'A' );
		  ELSE SET mLetterAsInt = mLetterAsInt + 1;
		END CASE;
		SET mLetter = CHAR( mLetterAsInt );
		SET mNumber = 1;
  END IF;
  SET mFileNumber = CONCAT( mLetter, RIGHT( CONCAT( "00000", mNumber ), 5 ) );
  INSERT INTO lcl_file_number ( file_number, created_datetime ) VALUES( mFileNumber, NOW() );
  SET mFileNumberID = LAST_INSERT_ID();
  SET mLock = RELEASE_LOCK( mLockName );
  IF ISNULL( mFileNumberID ) OR mFileNumberID < 1 THEN
	  SET mFileNumberID = NULL;
  END IF;
  RETURN mFileNumberID;
END MAIN$$
DELIMITER;

Suggested fix:
(shell script to test function)

#!/bin/bash
# NOTE: the ampersand after the database name - VERY IMPORTANT.
#
for ((i=1; i<= 100; i++));
do
  mysql \
    --host=192.168.56.50 \
    --user=root \
    -e "SELECT LCLFileNumberCreate()" \
    test &
done
#
# (end of file)
[19 Jun 2018 14:30] MySQL Verification Team
Hi,

Thank you for your bug report.

I think that you need to rewrite your function again. First, your timeout is too short for such a large number of threads, considering that stored routines are not executed at great speed.

You should also use BOOL variables properly. Last, but not least, you can help yourself with IS_IT_LOCKED() function that exists too ......
[20 Jul 2018 1:00] Bugs System
No feedback was provided for this bug for over a month, so it is
being suspended automatically. If you are able to provide the
information that was originally requested, please do so and change
the status of the bug back to "Open".