MySQL trigger + SELECT FOR UPDATE not locking - mysql

I’ve got a table of bookings with start and end times, and no two bookings can overlap.
I need to check that a new booking won’t overlap with any existing bookings. However we’ve got very high load so there’s a race condition: two overlapping bookings can be both successfully inserted because the first booking was inserted after the second booking checked for overlaps.
I’m trying to solve this by taking a lock on a related resource using a BEFORE INSERT database trigger.
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard BEFORE INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE overlapping_booking_resource_id INT DEFAULT NULL;
DECLARE msg VARCHAR(255);
-- Take an exclusive lock on the resource in question for the duration of the current
-- transaction. This will prevent double bookings.
DECLARE ignored INT DEFAULT NULL;
SELECT resource_id INTO ignored
FROM resource
WHERE resource_id = NEW.resource_id
FOR UPDATE;
-- Now we have the lock, check for optimistic locking conflicts:
SELECT booking_resource_id INTO overlapping_booking_resource_id
FROM booking_resource other
WHERE other.booking_from < NEW.booking_to
AND other.booking_to > NEW.booking_from
AND other.resource_id = NEW.resource_id
LIMIT 1;
IF overlapping_booking_resource_id IS NOT NULL THEN
SET msg = CONCAT('The inserted times overlap with booking_resource_id: ', overlapping_booking_resource_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END
//
If I put this trigger in the database and insert two bookings asynchronously from the command line, this trigger successfully blocks the overlapping booking. I’ve tried this with putting a SLEEP before the last IF statement in the trigger, to make sure that the lock has really been taken out.
However, I have a load testing environment in Jenkins which runs a lot of bookings concurrently using jMeter. When I put this trigger there and run the load tests, no overlapping bookings are caught, i.e. double bookings are made.
Some checks I’ve done:
I’ve logged out the SQL queries that the load test script generates when creating a booking, and it is the same as the SQL I use in the command line.
The trigger is definitely being triggered in the load test environment, and it is definitely not catching any overlapping bookings. I ascertained this by inserting the “overlapping_booking_resource_id” variable from the trigger into another table. All the values were null.
The trigger works in the load test environment when inserting bookings from the command line, i.e. it prevents the overlapping booking from being inserted.
If I make the constraint for what a “double booking” is slightly too strict, i.e. adjacent bookings count as double bookings, then I do see things being caught by the trigger – that is, the apache log records several errors with the message ‘The inserted times overlap with booking_resource_id:’
I’m wondering if maybe the lock is only taken out until the end of the trigger, and there is still a race condition between the end of the trigger and actually inserting into the table. However this doesn’t explain why none of the overlapping bookings are ever caught.
I’m really stuck now. Does anyone have any ideas as to what I have done wrong?

A less elegant but more robust method would be to use a table made for locking records accross the system.
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard BEFORE INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE overlapping_booking_resource_id INT DEFAULT NULL;
DECLARE msg VARCHAR(255);
-- Take an exclusive lock on the resource in question for the duration of the current
-- transaction. This will prevent double bookings.
---CHANGED HERE
REPEAT
BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING
BEGIN
SET locked = FALSE;
END;
locked=TRUE;
INSERT INTO lockresource values(NEW.resource_id);
END;
UNTIL LOCKED END REPEAT;
---TIL HERE
-- Now we have the lock, check for optimistic locking conflicts:
SELECT booking_resource_id INTO overlapping_booking_resource_id
FROM booking_resource other
WHERE other.booking_from < NEW.booking_to
AND other.booking_to > NEW.booking_from
AND other.resource_id = NEW.resource_id
LIMIT 1;
IF overlapping_booking_resource_id IS NOT NULL THEN
SET msg = CONCAT('The inserted times overlap with booking_resource_id: ', overlapping_booking_resource_id);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = msg;
END IF;
END
//
---ADDED FROM HERE
DELIMITER //
CREATE TRIGGER booking_resource_double_booking_guard_after AFTER INSERT ON booking_resource
FOR EACH ROW BEGIN
DECLARE CONTINUE HANDLER FOR SQLWARNING BEGIN END;
delete from lockresource where lockid=NEW.resource_id;
END
//
Anyway, that'd be the idea and would certainly prevent any loss of lock until completion of your validation.

Related

SQL unpredictable failure

I wrote an SQL stored procedure, that's called trough an SqlDataSource element (in a ASP.NET / C# web application).
To be more precise the procedure is called from within a trigger of tableA bound on the AFTER INSERT event:
CREATE PROCEDURE `create_checklist`(IN _id_A INT(10))
BEGIN
DECLARE _mode CHAR(4);
DECLARE _id_default INT(10);
SELECT
mode
FROM
tableA
WHERE
id = _id_A INTO _mode;
SELECT
id
FROM
tab_doc_type
WHERE
def_value = 1 INTO _id_default;
# LOCK TABLES TableA WRITE;
# START TRANSACTION;
INSERT INTO checklist
(id_type, id_A, priority)
SELECT id_doc_type, _id_A, priority
FROM tab_checklist
WHERE A_mode = _mode;
UPDATE
checklist
SET
not_needed = 1
WHERE
(id_A = _id_A ) AND (id_type = _id_default);
# COMMIT;
# UNLOCK TABLES;
END
CREATE TRIGGER `tableA_AINS` AFTER INSERT ON `tableA` FOR EACH ROW
BEGIN
CALL create_checklist(NEW.id);
IF NEW.mode = 'X' OR NEW.mode = 'Y' THEN
CALL create_booking(NEW.id);
END IF;
END
that should create the checklist entries for an element of the tableA and then update one of the checlist entries with a flag (not_needed ) with a predefined value.
My SQL backend is a MySQL server.
This works in most of the cases but users reports that 'sometime' the procedure fails without errors being shown, the checklist is not filled up so I'm assuming that INSERT command is not executed.
I'm not able to reproduce this behaviour.
I've been notified from a user the value of an _id_A on which the procedure failed and triggering it 'manually' from the MySQL SQL Editor worked withtout problems.
I tried to enclose lines of codes 'writing' data in table with LOCK TABLES .. UNLOCK TABLES and/or START TRANSACTION ... COMMIT without success. In both cases it seems that stored procedure cannot use such commands.
Anyone here is able to locate a possible failure reason?
There's a way to fix this?
In short there's a way to call the stored procedure from within the trigger and avoid race condition so the checklist table is correctly populated every time?

MySQL events - performance and limitations

I have set-up a simple event that runs every hour and adds a record like this:
ON SCHEDULE EVERY 1 HOUR STARTS '2015-01-01 00:00:00'
DO
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE a INT;
DECLARE cursor_1 CURSOR FOR SELECT item_id FROM item WHERE NOW()>expiration_date AND has_expired = 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cursor_1;
read_loop: LOOP
FETCH cursor_1 INTO a;
IF done THEN
LEAVE read_loop;
END IF;
UPDATE item SET has_expired=1 WHERE quote_id=a;
INSERT INTO item_log (item_id, message) VALUES (a, 'Item is now expired');
END LOOP;
END
This thing runs 24 times a day and it works as expected, however, there is another idea to create events dynamically and attach them with a given record, e.g.
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 3 WEEK
DO
BEGIN
UPDATE item SET has_expired=1 WHERE item_id=232;
INSERT INTO item_log (item_id, message) VALUES (232, 'Item is now expired');
END
Of course the above would have different values of interval and ids, but that would mean that there are possibly 1000s or tens of thousands of events.
Now, would that be a problem? Limitations and performance wise?
I can imagine that if there are no records, or just few created a month then first approach will be constantly running for nothing. However if there will be few items added an hour, then it will mean that DB could reach thousands of one-time events. Would that not cause problems of its own?
Would you like to run that 100% faster? Get rid of it. Instead, have SELECTs include the clause AND (expiration_date < NOW())
OK, so you asked about the code. Here are some comments:
The UPDATE and INSERT need to be in a transaction.
The SELECT needs FOR UPDATE and should be in the transaction, too. But this is less important because, unless you ever change expiration_date, it never matters.
Cursors suck, performance-wise. Select 100 rows to purge, then run one UPDATE and one INSERT.
Scanning the table for this flag will be a slow "table scan" unless you have an index starting with expiration_date.

MySQL Trigger not getting triggered?

So this should be a fairly straight forward trigger, but my MySQL isn't great, so it's undoubtably a failure on my part.
It's not updating the stats table at all, even though it should be;
DROP TRIGGER countryUpdate;
DELIMITER //
CREATE TRIGGER countryUpdate AFTER INSERT ON stats
FOR EACH ROW BEGIN
DECLARE NewIP varchar(16);
DECLARE NewCountry varchar(80);
SET NewIP = inet_aton(new.vis_ip);
SET NewCountry = (SELECT country FROM iptocountry WHERE lower_bound <= NewIP AND upper_bound >= NewIP)
UPDATE stats
SET Country = NewCountry
END //
DELIMITER;
Well, first off, your UPDATE—if it works at all—is changing all rows in the stats table, and its doing that for each row inserted. That really doesn't make much sense. At minimum, you want to add a where clause to only hit the one row you've just inserted.
Apparently, though, that can't work at all in MySQL, because "a stored function or trigger cannot modify a table that is already being used (for reading or writing) by the statement that invoked the function or trigger." (Look under “Restrictions for Stored Functions”)
So, instead, you need to use a a before insert trigger, and do a SET new.country = NewCountry to fix the row up before its ever inserted.

MySQL Stored Procedure Loop Problem

So I have a cursor called open_loop that does a query and now I want to loop through it, check if the value it holds exists in the database, if so I need to update that entry, if not insert it.
The problem is, the way I quit the loop is by waiting for a state 02000 which is: Success, but no rows found. Unfortunately, that also occurs if there is no entry in the user table, so the loop is cut right then! How can I make it not trigger for that event, but still trigger if loop_cur runs out of entries, or perhaps find a different way of exiting the loop?
My code is here:
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET done=1;
OPEN loop_cur;
REPEAT
FETCH loop_cur
INTO user_id, user_info
SELECT Id INTO user_id FROM users WHERE Id = user_id;
IF user_id IS NOT NULL THEN
UPDATE users
SET info = user_info
WHERE Id = user_id;
ELSE
INSERT INTO users (user_info);
END IF;
UNTIL done END REPEAT;
CLOSE loop_cur;
Please not that this is simplified since I'm really inserting and fetching many more values and the query for loop_cur is pretty complicated so I didn't include that, but it does work.
Finally, I cannot use INSERT ON DUPLICATE KEY UPDATE, what I'm checking isn't really a user_id, it's a different column, I used user_id for simplicity.
Use a boolean variable that is set to true within the loop.
Irrelevant code removed for simplicity:
DECLARE found boolean default false;
...
REPEAT
FETCH loop_cur ...
SET found = true;
...
UNTIL done END REPEAT;
-- found is now true if you got some rows, false otherwise

mysql and trigger usage question

I have a situation in which I don't want inserts to take place (the transaction should rollback) if a certain condition is met. I could write this logic in the application code, but say for some reason, it has to be written in MySQL itself (say clients written in different languages will be inserting into this MySQL InnoDB table) [that's a separate discussion].
Table definition:
CREATE TABLE table1(x int NOT NULL);
The trigger looks something like this:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
NEW.x = NULL;
END IF;
END;
I am guessing it could also be written as(untested):
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
ROLLBACK;
END IF;
END;
But, this doesn't work:
CREATE TRIGGER t1 BEFORE INSERT ON table1 ROLLBACK;
You are guaranteed that:
Your DB will always be MySQL
Table type will always be InnoDB
That NOT NULL column will always stay the way it is
Question: Do you see anything objectionable in the 1st method?
From the trigger documentation:
The trigger cannot use statements that explicitly or implicitly begin or end a transaction such as START TRANSACTION, COMMIT, or ROLLBACK.
Your second option couldn't be created. However:
Failure of a trigger causes the statement to fail, so trigger failure also causes rollback.
So Eric's suggestion to use a query that is guaranteed to result in an error is the next option. However, MySQL doesn't have the ability to raise custom errors -- you'll have false positives to deal with. Encapsulating inside a stored procedure won't be any better, due to the lack of custom error handling...
If we knew more detail about what your condition is, it's possible it could be dealt with via a constraint.
Update
I've confirmed that though MySQL has CHECK constraint syntax, it's not enforced by any engine. If you lock down access to a table, you could handle limitation logic in a stored procedure. The following trigger won't work, because it is referencing the table being inserted to:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
DECLARE num INT;
SET num = (SELECT COUNT(t.col)
FROM your_table t
WHERE t.col = NEW.col);
IF (num > 100) THEN
SET NEW.col = 1/0;
END IF;
END;
..results in MySQL error 1235.
Have you tried raising an error to force a rollback? For example:
CREATE TRIGGER t1 BEFORE INSERT ON table1
FOR EACH ROW
IF (condition) THEN
SELECT 1/0 FROM table1 LIMIT 1
END IF;
END;