From 85cce7d95f148c5ec025dd15bcf41d041202842a Mon Sep 17 00:00:00 2001 From: fmizzell Date: Tue, 25 Jun 2019 17:47:38 -0500 Subject: [PATCH] A cross-process lock. --- .circleci/config.yml | 26 ++++++++++++++ README.md | 7 ++++ composer.json | 19 ++++++++++ phpunit.xml | 18 ++++++++++ src/Locker.php | 86 ++++++++++++++++++++++++++++++++++++++++++++ test/LockerTest.php | 38 ++++++++++++++++++++ 6 files changed, 194 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/Locker.php create mode 100644 test/LockerTest.php diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..1cb36e6 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,26 @@ +version: 2.0 +jobs: + build: + environment: + CC_TEST_REPORTER_ID: b0e2dc22a6c4744ffb6a4a8597af2062cce268cbfd8c9baaaa660d79ed33a803 + docker: + - image: circleci/php:7-cli-node-browsers-legacy + working_directory: ~/repo + steps: + - checkout + - run: + name: Setup dependencies + command: | + sudo composer self-update + composer install -n --prefer-dist + - run: + name: Setup Code Climate test-reporter + command: | + curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + chmod +x ./cc-test-reporter + - run: + name: Run tests + command: | + ./cc-test-reporter before-build + vendor/bin/phpunit --testsuite all --coverage-clover clover.xml + ./cc-test-reporter after-build --coverage-input-type clover --exit-code $? diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3421f3 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Locker +![CircleCI (all branches)](https://img.shields.io/circleci/project/github/fmizzell/datastore.svg) +[![Maintainability](https://api.codeclimate.com/v1/badges/c04818119efc862221cd/maintainability)](https://codeclimate.com/github/fmizzell/locker/maintainability) +[![Test Coverage](https://api.codeclimate.com/v1/badges/c04818119efc862221cd/test_coverage)](https://codeclimate.com/github/fmizzell/locker/test_coverage) +[![GPLv3 license](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.en.html) + +A cross-process lock. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8cf6055 --- /dev/null +++ b/composer.json @@ -0,0 +1,19 @@ +{ + "name": "fmizzell/locker", + "description": "A cross-process lock.", + "license": "GPL-3.0-only", + "authors": [ + { + "name": "fmizzell", + "email": "fmizzell.dev@gmail.com" + } + ], + "autoload": { + "psr-4": { + "Locker\\": "src/" + } + }, + "require-dev": { + "phpunit/phpunit": "^7.4" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..b4d7c1e --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + test + + + + + + src + + + + diff --git a/src/Locker.php b/src/Locker.php new file mode 100644 index 0000000..523bf5a --- /dev/null +++ b/src/Locker.php @@ -0,0 +1,86 @@ +name = $name; + $this->expire = $expire; + $this->wait = $wait; + } + + /** + * Wait until we get the lock, or we get tired of waiting. + */ + public function getLock() { + do { + $this->wait(); + if ($this->timeWaited > $this->wait) { + $this->timeWaited = -1; + throw new \Exception("The lock was not acquire after {$this->wait} second(s)."); + } + $this->lockExpired(); + } while (!$this->lockCreated()); + + $this->timeWaited = -1; + return TRUE; + } + + /** + * Release the lock. + */ + public function releaseLock() { + $path = "/tmp/{$this->name}.lock"; + if (file_exists($path)) { + array_map('unlink', glob("{$path}/*.*")); + rmdir($path); + } + } + + private function wait() { + if ($this->timeWaited >= 0) { + sleep(1); + } + $this->timeWaited++; + } + + private function lockExpired() { + $file = "/tmp/{$this->name}.lock/expire.txt"; + if (file_exists($file) && file_get_contents($file) < time()) { + $this->releaseLock(); + return TRUE; + } + return FALSE; + } + + private function lockCreated() { + $lock_path = "/tmp/{$this->name}.lock"; + if (@mkdir($lock_path, 0700)) { + file_put_contents("{$lock_path}/expire.txt", (time() + $this->expire)); + return TRUE; + } + return FALSE; + } +} \ No newline at end of file diff --git a/test/LockerTest.php b/test/LockerTest.php new file mode 100644 index 0000000..da762a7 --- /dev/null +++ b/test/LockerTest.php @@ -0,0 +1,38 @@ +locker = new \Locker\Locker("test", 5, 1); + } + + public function testReleaseAndWait() { + $this->assertTrue($this->locker->getLock()); + + try { + $this->locker->getLock(); + } + catch(\Exception $e) { + $this->assertEquals($e->getMessage(), "The lock was not acquire after 1 second(s)."); + } + + $this->locker->releaseLock(); + $this->assertTrue($this->locker->getLock()); + } + + public function testExpiration() { + $this->assertTrue($this->locker->getLock()); + sleep(6); + $this->assertTrue($this->locker->getLock()); + } + + protected function tearDown() + { + $this->locker->releaseLock(); + } + +} \ No newline at end of file