refactor: Use the new locks for schema updates (#6931)

* Use the new locks for schema updates

* removed schema lock from discovery

* Add the ability to wait for a lock.
Add unit tests

* Add MysqlLock
Use that for the schema updates
Wait up to 30s for other schema updates to complete.

* Switch the schema lock back to a file lock for now.
Make FileLock support indefinite locking without polling.
Add a warning to MysqlLock for scenarios where it won't work.

* Delete MysqlLock.php

* Removed MySQL lock tests
This commit is contained in:
Tony Murray
2017-07-17 13:35:08 -05:00
committed by Neil Lathwood
parent 1e77d4b3ea
commit 181b0fb7d5
5 changed files with 167 additions and 135 deletions

View File

@@ -15,12 +15,13 @@ namespace LibreNMS;
class FileLock
{
private $name = "";
private $file = "";
private $name;
private $file;
/**
* @var resource | false
*/
private $handle = false;
private $handle;
private $acquired = false;
private function __construct($lock_name)
@@ -59,32 +60,41 @@ class FileLock
* Given a lock name, try to acquire the lock.
* On success return a FileLock object, or on failure return false.
* @param string $lock_name Name of lock
* @return mixed
* @param int $timeout Try for this many seconds to see if we can acquire the lock. Default is no wait. A negative timeout will wait forever.
* @return self|false
*/
public static function lock($lock_name)
public static function lock($lock_name, $timeout = 0)
{
$lock = new self($lock_name);
if ($lock->handle === false) {
return false;
}
if (flock($lock->handle, LOCK_EX | LOCK_NB)) {
$lock->acquired = true;
return $lock;
} else {
return false;
// try to acquire the lock each second until we reach the timeout, once if timeout is 0, forever if timeout < 0
for ($i = 0; $i <= $timeout || $timeout < 0; $i++) {
if (flock($lock->handle, $timeout < 0 ? LOCK_EX : LOCK_EX | LOCK_NB)) {
$lock->acquired = true;
return $lock;
}
if ($timeout) {
sleep(1);
}
}
return false;
}
/**
* Given a lock name, try to acquire the lock, exiting on failure.
* On success return a FileLock object.
* @param string $lock_name Name of lock
* @return FileLock
* @param int $timeout Try for this many seconds to see if we can acquire the lock. Default is no wait. A negative timeout will wait forever.
* @return self
*/
public static function lockOrDie($lock_name)
public static function lockOrDie($lock_name, $timeout = 0)
{
$lock = self::lock($lock_name);
$lock = self::lock($lock_name, $timeout);
if ($lock === false) {
echo "Failed to acquire lock $lock_name, exiting\n";

View File

@@ -114,10 +114,6 @@ if (!$where) {
exit;
}
if (get_lock('schema') === false) {
require 'includes/sql-schema/update.php';
}
update_os_cache(); // will only update if needed
$discovered_devices = 0;

View File

@@ -2334,53 +2334,6 @@ function db_schema_is_current()
return $current >= $latest;
}
/**
* Get the lock status for a given name
* @param $name
* @return bool
*/
function get_lock($name)
{
global $config;
$lock_file = $config['install_dir']."/.$name.lock";
if (file_exists($lock_file)) {
$pids = explode("\n", trim(`ps -e | grep php | awk '{print $1}'`));
$lpid = trim(file_get_contents($lock_file));
if (in_array($lpid, $pids)) {
return true;
}
}
return false;
}
/**
* Set the lock status for a given name
* @param $name
*/
function set_lock($name)
{
global $config;
$lock_file = $config['install_dir']."/.$name.lock";
$lock = get_lock($name);
if ($lock === true) {
echo "$lock_file exists, exiting\n";
exit(1);
} else {
file_put_contents($lock_file, getmypid());
}
}
/**
* Release lock file for a given name
* @param $name
*/
function release_lock($name)
{
global $config;
unlink($config['install_dir']."/.$name.lock");
}
/**
* @param $device
* @return int|null

View File

@@ -1,8 +1,5 @@
<?php
// MYSQL Check - FIXME
// 1 UNKNOWN
/*
* LibreNMS Network Management and Monitoring System
* Copyright (C) 2006-2012, Observium Developers - http://www.observium.org
@@ -21,93 +18,97 @@ if (!isset($init_modules) && php_sapi_name() == 'cli') {
require realpath(__DIR__ . '/../..') . '/includes/init.php';
}
set_lock('schema');
$schemaLock = \LibreNMS\FileLock::lock('schema', 30);
if ($schemaLock === false) {
echo "Failed to acquire lock, skipping schema update\n";
$return = 1;
} else {
$return = 0;
$return = 0;
// only import build.sql to an empty database
$tables = dbFetchRows("SHOW TABLES FROM {$config['db_name']}");
if (empty($tables)) {
echo "-- Creating base database structure\n";
$step = 0;
$sql_fh = fopen('build.sql', 'r');
if ($sql_fh === false) {
echo 'ERROR: Cannot open SQL build script ' . $sql_file . PHP_EOL;
$return = 1;
}
// only import build.sql to an empty database
$tables = dbFetchRows("SHOW TABLES FROM {$config['db_name']}");
if (empty($tables)) {
echo "-- Creating base database structure\n";
$step = 0;
$sql_fh = fopen('build.sql', 'r');
if ($sql_fh === false) {
echo 'ERROR: Cannot open SQL build script '.$sql_file.PHP_EOL;
$return = 1;
}
while (!feof($sql_fh)) {
$line = fgetss($sql_fh);
echo 'Step #' . $step++ . ' ...' . PHP_EOL;
while (!feof($sql_fh)) {
$line = fgetss($sql_fh);
echo 'Step #'.$step++.' ...'.PHP_EOL;
if (!empty($line)) {
$creation = dbQuery($line);
if (!$creation) {
echo 'WARNING: Cannot execute query ('.$line.'): '.mysqli_error($database_link)."\n";
$return = 1;
if (!empty($line)) {
$creation = dbQuery($line);
if (!$creation) {
echo 'WARNING: Cannot execute query (' . $line . '): ' . mysqli_error($database_link) . "\n";
$return = 1;
}
}
}
fclose($sql_fh);
}
fclose($sql_fh);
}
d_echo("DB Schema update started....\n");
d_echo("DB Schema update started....\n");
if (db_schema_is_current()) {
d_echo("DB Schema already up to date.\n");
} else {
// Set Database Character set and Collation
dbQuery('ALTER DATABASE ? CHARACTER SET utf8 COLLATE utf8_unicode_ci;', array(array($config['db_name'])));
if (db_schema_is_current()) {
d_echo("DB Schema already up to date.\n");
} else {
// Set Database Character set and Collation
dbQuery('ALTER DATABASE ? CHARACTER SET utf8 COLLATE utf8_unicode_ci;', array(array($config['db_name'])));
$db_rev = get_db_schema();
$insert = ($db_rev == 0); // if $db_rev == 0, insert the first update
$db_rev = get_db_schema();
$insert = ($db_rev == 0); // if $db_rev == 0, insert the first update
$updating = 0;
foreach (get_schema_list() as $file_rev => $file) {
if ($file_rev > $db_rev) {
if (!$updating) {
echo "-- Updating database schema\n";
}
$updating = 0;
foreach (get_schema_list() as $file_rev => $file) {
if ($file_rev > $db_rev) {
if (!$updating) {
echo "-- Updating database schema\n";
}
printf('%03d -> %03d ...', $db_rev, $file_rev);
printf('%03d -> %03d ...', $db_rev, $file_rev);
$err = 0;
if ($data = file_get_contents($file)) {
foreach (explode("\n", $data) as $line) {
if (trim($line)) {
d_echo("$line \n");
$err = 0;
if ($data = file_get_contents($file)) {
foreach (explode("\n", $data) as $line) {
if (trim($line)) {
d_echo("$line \n");
if ($line[0] != '#') {
if (!mysqli_query($database_link, $line)) {
$err++;
d_echo(mysqli_error($database_link) . PHP_EOL);
if ($line[0] != '#') {
if (!mysqli_query($database_link, $line)) {
$err++;
d_echo(mysqli_error($database_link) . PHP_EOL);
}
}
}
}
echo " done ($err errors).\n";
} else {
echo " Could not open file! $file\n";
$return = 1;
}//end if
$updating++;
$db_rev = $file_rev;
if ($insert) {
dbInsert(array('version' => $db_rev), 'dbSchema');
$insert = false;
} else {
dbUpdate(array('version' => $db_rev), 'dbSchema');
}
echo " done ($err errors).\n";
} else {
echo " Could not open file! $file\n";
$return = 1;
}//end if
}//end foreach
$updating++;
$db_rev = $file_rev;
if ($insert) {
dbInsert(array('version' => $db_rev), 'dbSchema');
$insert = false;
} else {
dbUpdate(array('version' => $db_rev), 'dbSchema');
}
}//end if
}//end foreach
if ($updating) {
echo "-- Done\n";
if ($updating) {
echo "-- Done\n";
}
}
}
release_lock('schema');
$schemaLock->release();
}

72
tests/LockTest.php Normal file
View File

@@ -0,0 +1,72 @@
<?php
/**
* LockTest.php
*
* Test Locking functionality.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2017 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Tests;
use LibreNMS\FileLock;
use PHPUnit\Framework\TestCase;
class LockTest extends TestCase
{
public function testFileLock()
{
$lock = FileLock::lock('tests');
$this->assertNotFalse($lock, 'Failed to acquire initial lock!');
$lock->release();
$new_lock = FileLock::lock('tests');
$this->assertNotFalse($new_lock, 'Failed to release the lock with release()');
unset($new_lock);
$this->assertNotFalse(FileLock::lock('tests'), 'Failed to remove lock when the lock object was destroyed');
}
public function testFileLockFail()
{
$lock = FileLock::lock('tests');
$this->assertNotFalse($lock, 'Failed to acquire initial lock!');
$failed_lock = FileLock::lock('tests');
$this->assertFalse($failed_lock, 'Additional lock attempt did not fail');
}
public function testFileLockWait()
{
$lock = FileLock::lock('tests');
$this->assertNotFalse($lock, 'Failed to acquire initial lock!');
$start = microtime(true);
$wait_lock = FileLock::lock('tests', 1);
$this->assertGreaterThan(1, microtime(true) - $start, 'Lock did not wait.');
$this->assertFalse($wait_lock, 'Waiting lock attempt did not fail');
$lock->release();
$start = microtime(true);
$wait_lock = FileLock::lock('tests', 5);
$this->assertLessThan(1, microtime(true) - $start, 'Lock waited when it should not have');
$this->assertNotFalse($wait_lock, 'Second wait lock did not succeed');
}
}