8
votes

I'm doing a bash script that interacts with a MySQL datatabase using the mysql command line programme. I want to use table locks in my SQL. Can I do this?

mysql -e "LOCK TABLES mytable"
# do some bash stuff
mysql -u "UNLOCK TABLES"

The reason I ask, is because table locks are only kept for the session, so wouldn't the lock be released as soon as that mysql programme finishes?

5

5 Answers

8
votes

[EDIT]

nos had the basic idea -- only run "mysql" once, and the solution nos provided should work, but it left the FIFO on disk.

nos was also correct that I screwed up: a simple "echo X >FIFO" will close the FIFO; I remembered wrongly. And my (removed) comments w.r.t. timing don't apply, sorry.

That said, you don't need a FIFO, you could use an inter-process pipe. And looking through my old MySQL scripts, some worked akin to this, but you cannot let any commands write to stdout (without some "exec" tricks).

#!/bin/bash
(
  echo "LOCK TABLES mytable READ ;"
  echo "Doing something..." >&2
  echo "describe mytable;" 
  sleep 5
  echo "UNLOCK  tables;" 
) | mysql ${ARGUMENTS}

Another option might be to assign a file descriptor to the FIFO, then have it run in the background. This is very similar to what nos did, but the "exec" option wouldn't require a subshell to run the bash commands; hence would allow you to set "RC" in the "other stuff":

#!/bin/bash
# Use the PID ($$) in the FIFO and remove it on exit:
FIFO="/tmp/mysql-pipe.$$"
mkfifo ${FIFO} || exit $?
RC=0

# Tie FD3 to the FIFO (only for writing), then start MySQL in the u
# background with its input from the FIFO:
exec 3<>${FIFO}

mysql ${ARGUMENTS} <${FIFO} &
MYSQL=$!
trap "rm -f ${FIFO};kill -1 ${MYSQL} 2>&-" 0

# Now lock the table...
echo "LOCK TABLES mytable WRITE;" >&3

# ... do your other stuff here, set RC ...
echo "DESCRIBE mytable;" >&3
sleep 5
RC=3
# ...

echo "UNLOCK TABLES;" >&3
exec 3>&-

# You probably wish to sleep for a bit, or wait on ${MYSQL} before you exit
exit ${RC}

Note that there are a few control issues:

  • This code has NO ERROR CHECKING for failure to lock (or any SQL commands within the "other stuff"). And that's definitely non-trivial.
  • Since in the first example, the "other stuff" is within a subshell, you cannot easily set the return code of the script from that context.
4
votes

Here's one way, I'm sure there's an easier way though..

mkfifo /tmp/mysql-pipe
mysql mydb </tmp/mysql-pipe &
(
  echo "LOCK TABLES mytable READ ;" 1>&6 
  echo "Doing something "
  echo "UNLOCK  tables;" 1>&6
) 6> /tmp/mysql-pipe
2
votes

A very interesting approach I found out while looking into this issue for my own, is by using MySQL's SYSTEM command. I'm not still sure what exactly are the drawbacks, if any, but it will certainly work for a lot of cases:

Example:

mysql <<END_HEREDOC
LOCK TABLES mytable;
SYSTEM /path/to/script.sh
UNLOCK TABLES;
END_HEREDOC

It's worth noting that this only works on *nix, obviously, as does the SYSTEM command.

Credit goes to Daniel Kadosh: http://dev.mysql.com/doc/refman/5.5/en/lock-tables.html#c10447

0
votes

Another approach without the mkfifo commands:

cat <(echo "LOCK TABLES mytable;") <(sleep 3600) | mysql &
LOCK_PID=$!
# BASH STUFF
kill $LOCK_PID

I think Amr's answer is the simplest. However I wanted to share this because someone else may also need a slightly different answer.

The sleep 3600 pauses the input for 1 hour. You can find other commands to make it pause here: https://unix.stackexchange.com/questions/42901/how-to-do-nothing-forever-in-an-elegant-way

The lock tables SQL runs immediately, then it will wait for the sleep timer.

0
votes

Problem and limitation in existing answers

  • Answers by NVRAM, nos and xer0x

    If commands between LOCK TABLES and UNLOCK TABLES are all SQL queries, you should be fine. In this case, however, why don't we just simply construct a single SQL file and pipe it to the mysql command?

    If there are commands other than issuing SQL queries in the critical section, you could be running into trouble. The echo command that sends the lock statement to the file descriptor doesn't block and wait for mysql to respond. Subsequent commands are therefore possible to be executed before the lock is actually acquired. Synchronization aren't guaranteed.

  • Answer by Amr Mostafa

    The SYSTEM command is executed on the MySQL server. So the script or command to be executed must be present on the same MySQL server. You will need terminal access to the machine/VM/container that host the server (or at least a mean to transfer your script to the server host). SYSTEM command also works on Windows as of MySQL 8.0.19, but running it on a Windows server of course means you will be running a Windows command (e.g. batch file or PowerShell script).

A modified solution

Below is a example solution based on the answers by NVRAM and nos, but waits for lock:

#!/bin/bash

# creates named pipes for attaching to stdin and stdout of mysql
mkfifo /tmp/mysql.stdin.pipe /tmp/mysql.stdout.pipe

# unbuffered option to ensure mysql doesn't buffer the output, so we can read immediately
# batch and skip-column-names options are for ease of parsing the output
mysql --unbuffered --batch --skip-column-names $OTHER_MYSQL_OPTIONS < /tmp/mysql.stdin.pipe > /tmp/mysql.stdout.pipe &
PID_MYSQL=$!

# make sure to stop mysql and remove the pipes before leaving
cleanup_proc_pipe() {
    kill $PID_MYSQL
    rm -rf /tmp/mysql.stdin.pipe /tmp/mysql.stdout.pipe
}
trap cleanup_proc_pipe EXIT

# open file descriptors for writing and reading
exec 10>/tmp/mysql.stdin.pipe
exec 11</tmp/mysql.stdout.pipe

# update the cleanup procedure to close the file descriptors
cleanup_fd() {
    exec 10>&-
    exec 11>&-
    cleanup_proc_pipe
}
trap cleanup_fd EXIT

# try to obtain lock with 5 seconds of timeout
echo 'SELECT GET_LOCK("my_lock", 5);' >&10

# read stdout of mysql with 6 seconds of timeout
if ! read -t 6 line <&11; then
    echo "Timeout reading from mysql"
elif [[ $line == 1 ]]; then
    echo "Lock acquired successfully"
    echo "Doing some critical stuff..."
    echo 'DO RELEASE_LOCK("my_lock");' >&10
else
    echo "Timeout waiting for lock"
fi

The above example uses SELECT GET_LOCK() to enter the critical section. It produces output for us to parse the result and decide what to do next. If you need to execute statements that doesn't produce output (e.g. LOCK TABLES and START TRANSACTION), you may perform a dummy SELECT 1; after such statement and read from the stdout with a reasonable timeout. E.g.:

# ...
echo 'LOCK TABLES my_table WRITE;' >&10
echo 'SELECT 1;' >&10
if ! read -t 10 line <&11; then
    echo "Timeout reading from mysql"
elif [[ $line == 1 ]]; then
    echo "Table lock acquired"
    # ...
else
    echo "Unexpected output?!"
fi

You may also want to attach a third named pipe to stderr of mysql to handle different cases of error.