Skip to content

Commit

Permalink
Release version 1.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
jakubboucek committed Aug 13, 2021
2 parents f123b04 + 8e2da20 commit fd8af19
Show file tree
Hide file tree
Showing 14 changed files with 562 additions and 11 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/vendor/
composer.lock
/composer.lock
/examples/lock/temp
/vendor/
/tests/Lock/lock
/tests/Lock/temp
12 changes: 12 additions & 0 deletions .phpstorm.meta.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace PHPSTORM_META;

expectedArguments(
\Redbitcz\Utils\Lock\Locker::__construct(),
2,
\Redbitcz\Utils\Lock\Locker::BLOCKING,
\Redbitcz\Utils\Lock\Locker::NON_BLOCKING
);
85 changes: 83 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,102 @@ related method call will be siletly ignored.

## Usage

### `Locker`

The `\Redbitcz\Utils\Lock\Locker` class is simple implementation of lock/semaphor based of filelock. It's optimized for
Linux architecture.

Locker support two modes:

- **Blocking mode** – Blocking mode is create semaphor for locked space, all concurrent locks will **wait to release
previous lock**. Be careful, it may cause to deadlock of PHP processes, because lock at filesystem is not subject of
[`max_execution_time`](https://www.php.net/manual/en/info.configuration.php#ini.max-execution-time) limit!
- **Non blocking mode** – Non blocking mode is create lock which is prevent access concurrent processes to locked stage.
All concurent locks **will imediatelly fails** with `LockObtainException` Exception.

Example non-blocking lock:

```php
$locker = new Locker(__DIR__, 'example', Locker::NON_BLOCKING);

try {
$locker->lock();

// ... exclusive operation

$locker->unlock();
}
catch (LockObtainException $e) {
die('Error: Another process is alreasy processing that stuff');
}
```

See [Non-blocking `Locker` example](examples/lock/non-blocking-locker.php).

Example blocking lock:

```php
$locker = new Locker(__DIR__, 'example', Locker::BLOCKING);

$locker->lock(); // concurrent process will be wait here to release previous lock

// ... exclusive operation

$locker->unlock();
```

See [Blocking `Locker` example](examples/lock/blocking-locker.php).

### `Logger`

The `\Redbitcz\Utils\Log\Logger` class is implementation of PSR-3 logger interface and it decorates each
logger record with time and log severity name.

Example:
```
[2021-05-05 11:49:36] Logged message 1
[2021-05-05 11:49:38] Another logged message
[2021-05-05 11:49:36] INFO: Logged message 1
[2021-05-05 11:49:38] DEBUG: Another logged message
```

Logger requires Writer `\Redbitcz\Utils\IO\IOutStream` instance. Package contains few several types
of Writer implementations which are different by the log target (console, general output, standard output, HTML output,
or file).

Logger also support sectionalization for long-processing operations:

Example:

```php
$logger->info("Processing message: $messageId");

$messageLogger = $logger->section($messageId);
$messageLogger->info('Open');

function parse(LoggerInterface $parserLogger) {
$parserLogger->info('Parsing...');
// ...
$parserLogger->info('Parsing OK');
}

parse($messageLogger->section('parser'));

$messageLogger->info('Save');

$logger->info('Done');
```

Sends to output:
```
[2021-05-05 11:49:36] INFO: Processing message: 123456789
[2021-05-05 11:49:37] INFO: {123456789} Open
[2021-05-05 11:49:38] INFO: {123456789/parser} Parsing...
[2021-05-05 11:49:38] INFO: {123456789/parser} Parsing OK
[2021-05-05 11:49:38] INFO: {123456789} Save
[2021-05-05 11:49:36] INFO: Done
```

Section is useful to provide logger to another service which is requested to process single entity.

See [`Logger` example](examples/log/output-logger.php).

### `Progress`
Expand Down
32 changes: 32 additions & 0 deletions examples/lock/blocking-locker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

use Redbitcz\Utils\Lock\Locker;

require_once __DIR__ . '/../../vendor/autoload.php';

$tempDir = __DIR__ . '/temp';

if (!@mkdir($tempDir) && !is_dir($tempDir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $tempDir));
}

$locker = new Locker($tempDir, basename(__FILE__), Locker::BLOCKING);

echo "== Example od Blocking locker ==\n\n";

echo "Locker is lock following code of script to 30 seconds, try to run same script from another PHP process.\n";

$locker->lock();

echo "Locker is successfully locked.\n";

for ($i = 0; $i < 30; $i++) {
echo ".";
sleep(1);
}

$locker->unlock();

echo "\nLocker is unlocked. Done.\n";
37 changes: 37 additions & 0 deletions examples/lock/non-blocking-locker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

use Redbitcz\Utils\Lock\Exception\LockObtainException;
use Redbitcz\Utils\Lock\Locker;

require_once __DIR__ . '/../../vendor/autoload.php';

$tempDir = __DIR__ . '/temp';

if (!@mkdir($tempDir) && !is_dir($tempDir)) {
throw new RuntimeException(sprintf('Directory "%s" was not created', $tempDir));
}

$locker = new Locker($tempDir, basename(__FILE__), Locker::NON_BLOCKING);

echo "== Example od Non-blocking locker ==\n\n";

echo "Locker is lock following code of script to 30 seconds, try to run same script from another PHP process.\n";

try {
$locker->lock();

echo "Locker is successfully locked.\n";

for ($i = 0; $i < 30; $i++) {
echo ".";
sleep(1);
}

$locker->unlock();

echo "\nLocker is unlocked. Done.\n";
} catch (LockObtainException $e) {
echo 'Error: lock is alreasy locked by another process! ' . $e->getMessage();
}
28 changes: 24 additions & 4 deletions examples/log/output-logger.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,33 @@

declare(strict_types=1);

use Redbitcz\Utils\Log\Logger;
use Psr\Log\LoggerInterface;
use Redbitcz\Utils\IO\OutputWriter;
use Redbitcz\Utils\Log\Logger;

require_once __DIR__ . '/../../vendor/autoload.php';

$writer = new OutputWriter();
$outputLogger = new Logger($writer);
$logger = new Logger($writer);

$logger->debug('This is Logger demo:');
$logger->info('Here is {variable}', ['variable' => 'Hello World']);

$logger->debug('This is SectionLogger demo:');

$messages = ['foo', 'bar', 'baz', 'foo bar'];
$logger->info('Prepared {count} messages to process', ['count' => count($messages)]);

foreach ($messages as $messageKey => $message) {
processMessage($message, $logger->section("Message ID $messageKey"));
}

function processMessage(string $message, LoggerInterface $logger)
{
$logger->info('Processing message with text: "{message}"', ['message' => $message]);

$message = strrev($message);
$logger->info('OK');

$outputLogger->info('This is info message.');
$outputLogger->debug('Here is {variable}', ['variable' => 'Hello World']);
$logger->info('Procesed mesage is: "{message}"', ['message' => $message]);
}
12 changes: 12 additions & 0 deletions src/Lock/Exception/DirectoryNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Redbitcz\Utils\Lock\Exception;

use LogicException;

class DirectoryNotFoundException extends LogicException

{
}
11 changes: 11 additions & 0 deletions src/Lock/Exception/LockObtainException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Redbitcz\Utils\Lock\Exception;

use RuntimeException;

class LockObtainException extends RuntimeException
{
}
106 changes: 106 additions & 0 deletions src/Lock/Locker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace Redbitcz\Utils\Lock;

use Redbitcz\Utils\Lock\Exception\DirectoryNotFoundException;
use Redbitcz\Utils\Lock\Exception\LockObtainException;

class Locker
{
/**
* `BLOCKING` mode - Blocking lock is waiting to release previous lock.
* Be careful, it may cause to deadlock of PHP processes, because lock at filesystem is not subject of
* `max_execution_time` limit!
*/
public const BLOCKING = 0;

/**
* `NON_BLOCKING` mode - Non blocking lock is fail immediately with the Exception when lock obtaining is failed.
*/
public const NON_BLOCKING = LOCK_NB;

/** @var string */
private $lockFile;

/** @var resource|null */
private $lockHandle;

/** @var int */
private $lockMode;

/**
* @param string $dir Path to lockfiles directory, must exists and must be writable.
* @param string $name Lock name. Lockers with the same `$dir` and `$name` are interlocking.
* @param int $blockMode `BLOCKING` is waiting to release previous lock, `NON_BLOCKING` doesn't wait, it fails immediately
*/
public function __construct(string $dir, string $name, int $blockMode = self::NON_BLOCKING)
{
if (!is_dir($dir)) {
throw new DirectoryNotFoundException("Directory '$dir' not found.");
}

$this->lockFile = $this->getLockfileName($dir, $name);
$this->lockMode = LOCK_EX | $blockMode;
}

public function __destruct()
{
$this->unlock();
}

/**
* Try to obtain lock, throw `LockObtainException` when lock obtaining failed or wait to release previous lock
*
* @throws LockObtainException
* @see `\Redbitcz\Utils\Lock\Locker::__construct()` documentation of `$blockMode` argument
*/
public function lock(): void
{
if ($this->lockHandle) {
return;
}

$lockHandle = @fopen($this->lockFile, 'c+b'); // @ is escalated to exception
if ($lockHandle === false) {
$error = (error_get_last()['message'] ?? 'unknown error');
throw new LockObtainException("Unable to create lockfile '{$this->lockFile}', $error");
}

if (@flock($lockHandle, $this->lockMode, $alreadyLocked) === false) { // @ is escalated to exception
throw new LockObtainException("Unable to obtain exclusive lock on lockfile '{$this->lockFile}'");
}

// Prevent leak already locked advisory lock
if (($this->lockMode & self::BLOCKING) === 0 && $alreadyLocked === 1) {
@flock($lockHandle, LOCK_UN);
throw new LockObtainException(
"Unable to obtain exclusive lock on lockfile '{$this->lockFile}', already locked"
);
}

$this->lockHandle = $lockHandle;
}

/**
* Unlock current lock and try to remove lockfile
*/
public function unlock(): void
{
if ($this->lockHandle === null) {
return;
}

flock($this->lockHandle, LOCK_UN);
fclose($this->lockHandle);
$this->lockHandle = null;

@unlink($this->lockFile); // @ file may not exists yet (purge tempdir, etc.)
}

private function getLockfileName(string $tempDir, string $namespace): string
{
return $tempDir . '/rbt_' . urlencode($namespace) . '.lock';
}
}
Loading

0 comments on commit fd8af19

Please sign in to comment.