Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,41 @@ logging should be emitted. If not set, logging is automatically-enabled.
* Use the boolean flag `$CFG->local_redislock_disable_shared_connection` to force creation
of the redis connection for each factory instance.

## Customising the lock identity payload
The value stored alongside each Redis lock is produced by an `identity_provider` service. By default it records the hostname and the current PHP process ID, but you can override the DI binding to record whatever metadata you need (for example an AWS ARN, container identifier).

Registering an override inside another plugin via **local/my_plugin/classes/hook_callbacks.php**:

```php
namespace local_my_plugin

class hook_callbacks {
public static function di_configuration(\core\hook\di_configuration $hook): void {
$hook->add_definition(
\local_redislock\lock\identity_provider::class,
\DI\autowire(\local_yourplugin\lock\arn_identity_provider::class),
);
}
}
```

Because local_redislock registers its DI callback with priority 999, any plugin using the default priority (100) will run afterwards and automatically overwrite the binding without extra effort.

### Override via config.php
If it's ever needed to restore the default identity provider or change priorities without touching code, hook priorities can be adjusted via **config.php**:

```php
$CFG->hooks_callback_overrides = [
\core\hook\di_configuration::class => [
'\local_redislock\hook_callbacks::di_configuration' => [
'priority' => 0, // Ensure redislock runs last, restoring the default identity provider.
],
],
];
```

Setting `'disabled' => true` for another plugin's callback will stop it from overriding the provider entirely.

## License
Copyright (c) 2021 Open LMS (https://www.openlms.net)

Expand Down
43 changes: 43 additions & 0 deletions classes/hook_callbacks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.

declare(strict_types=1);

namespace local_redislock;

use core\hook\di_configuration;
use local_redislock\lock\default_identity_provider;
use local_redislock\lock\identity_provider;

use function DI\autowire;

/**
* Hook callbacks for the Redis lock plugin.
*
* @package local_redislock
* @copyright 2025 Cameron Ball <cameron@cameron1729.xyz>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class hook_callbacks {
/**
* Register DI definitions.
*
* @param di_configuration $hook
*/
public static function di_configuration(di_configuration $hook): void {
$hook->add_definition(identity_provider::class, autowire(default_identity_provider::class));
}
}
37 changes: 37 additions & 0 deletions classes/lock/default_identity_provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.

declare(strict_types=1);

namespace local_redislock\lock;

/**
* Default identity provider for Redis locks.
*
* @package local_redislock
* @copyright 2025 Cameron Ball <cameron@cameron1729.xyz>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class default_identity_provider implements identity_provider {
/**
* Build default metadata.
*
* @return array Associative array of hostname and process ID.
*/
public function build(): array {
return ['hostname' => gethostname() ?: 'UNKNOWN', 'processid' => (string)getmypid()];
}
}
40 changes: 40 additions & 0 deletions classes/lock/identity_provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.

declare(strict_types=1);

namespace local_redislock\lock;

/**
* Interface for the metadata stored inside Redis locks.
*
* Implementations must return the same associative array for the lifetime
* of the current process, otherwise releases will fail because the stored value
* will not match. Keys and values in the returned associative array must be strings.
*
* @package local_redislock
* @copyright 2025 Cameron Ball <cameron@cameron1729.xyz>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
interface identity_provider {
/**
* Produce the identifying metadata for the current process.
*
* @return array Associative array that will be encoded into the lock value.
* Keys and values must be strings.
*/
public function build(): array;
}
47 changes: 39 additions & 8 deletions classes/lock/redis_lock_factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,12 @@

namespace local_redislock\lock;

use core\di;
use core\exception\coding_exception;
use core\lock\lock_factory;
use core\lock\lock;
use local_redislock\api\shared_redis_connection;
use local_redislock\lock\identity_provider;

/**
* Redis-backed lock factory class.
Expand Down Expand Up @@ -79,13 +82,19 @@ class redis_lock_factory implements lock_factory {
*/
private $auth;

/**
* @var identity_provider Identity provider service.
*/
private identity_provider $identityprovider;

/**
* @param string $type The type this lock is used for (e.g. cron, cache).
* @param \Redis|null $redis An instance of the PHPRedis extension class.
* @param boolean|null $logging Should verbose logs be emitted.
* @param identity_provider|null $identityprovider Identity provider service.
* @throws \core\exception\coding_exception
*/
public function __construct($type, \Redis $redis = null, $logging = null) {
public function __construct($type, ?\Redis $redis = null, $logging = null, ?identity_provider $identityprovider = null) {
global $CFG;

$this->type = $type;
Expand All @@ -103,11 +112,12 @@ public function __construct($type, \Redis $redis = null, $logging = null) {
// Logging enabled only for CLI, web gets damaged by lock logs.
$logging = (CLI_SCRIPT && debugging() && !PHPUNIT_TEST);
if (isset($CFG->local_redislock_logging)) {
$logging = $this->logging && ((bool) $CFG->local_redislock_logging);
$logging = $logging && ((bool) $CFG->local_redislock_logging);
}
}
$this->redis = $redis;
$this->logging = $logging;
$this->identityprovider = $identityprovider ?? di::get(identity_provider::class);

if (!PHPUNIT_TEST) {
\core_shutdown_manager::register_function(array($this, 'auto_release'));
Expand Down Expand Up @@ -404,7 +414,7 @@ protected function log($message) {
}

/**
* Returns the hostname or 'UNKNOWN' for use in the lock value.
* Returns the hostname or 'UNKNOWN' for logging purposes.
*
* @return string
*/
Expand All @@ -418,12 +428,33 @@ protected function get_hostname() {
/**
* Get the value that should be used for the lock.
*
* @return string
* @return string The lock value.
* @throws coding_exception If the identity metadata is invalid.
*/
protected function get_lock_value() {
return http_build_query(array(
'hostname' => $this->get_hostname(),
'processid' => getmypid(),
), '', '&');
$data = $this->identityprovider->build();
if (!is_array($data) || empty($data)) {
throw new coding_exception(
'Redis lock identity provider must return a non-empty array of metadata.',
);
}

$stringkeys = array_filter(array_keys($data), static fn($key) => is_string($key));
if (count($data) !== count($stringkeys)) {
throw new coding_exception(
'Redis lock identity provider keys must be strings.',
);
}

$stringvalues = array_filter(array_values($data), static fn($value) => is_string($value));
if (count($data) !== count($stringvalues)) {
throw new coding_exception(
'Redis lock identity provider values must be strings.',
);
}

ksort($data);

return http_build_query($data, '', '&');
}
}
40 changes: 40 additions & 0 deletions db/hooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php
// This file is part of Moodle - https://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle. If not, see <https://www.gnu.org/licenses/>.

/**
* Hook callbacks.
*
* @package local_redislock
* @copyright Cameron Ball <cameron@cameron1729.xyz>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

declare(strict_types=1);

defined('MOODLE_INTERNAL') || die();

$callbacks = [
[
// The hostname resolver is retrieved via DI, and we provide the default
// implementation by listening to the DI configuration hook.
'hook' => \core\hook\di_configuration::class,
'callback' => [\local_redislock\hook_callbacks::class, 'di_configuration'],
// Higher priority means "runs sooner". Setting 999 here means pretty much
// any plugin that listens to this hook will run after us, allowing them
// to switch in their own hostname resolver.
'priority' => 999,
],
];
2 changes: 1 addition & 1 deletion version.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
defined('MOODLE_INTERNAL') || die();

/** @var object $plugin */
$plugin->version = 2025060500; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2025111200; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2024100700; // Requires this Moodle version.
$plugin->component = 'local_redislock'; // Full name of the plugin (used for diagnostics).
$plugin->maturity = MATURITY_STABLE;
Expand Down