Merge pull request #677 from Zankaria/refactor-queue

Refactor queue
This commit is contained in:
Lorenzo Yario 2024-04-03 12:17:50 -07:00 committed by GitHub
commit 98650ec2e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 188 additions and 81 deletions

View File

@ -428,10 +428,10 @@ function rebuildThemes($action, $boardname = false) {
$board = $_board; $board = $_board;
// Reload the locale // Reload the locale
if ($config['locale'] != $current_locale) { if ($config['locale'] != $current_locale) {
$current_locale = $config['locale']; $current_locale = $config['locale'];
init_locale($config['locale']); init_locale($config['locale']);
} }
if (PHP_SAPI === 'cli') { if (PHP_SAPI === 'cli') {
echo "Rebuilding theme ".$theme['theme']."... "; echo "Rebuilding theme ".$theme['theme']."... ";
@ -450,8 +450,8 @@ function rebuildThemes($action, $boardname = false) {
// Reload the locale // Reload the locale
if ($config['locale'] != $current_locale) { if ($config['locale'] != $current_locale) {
$current_locale = $config['locale']; $current_locale = $config['locale'];
init_locale($config['locale']); init_locale($config['locale']);
} }
} }
@ -2918,7 +2918,20 @@ function generation_strategy($fun, $array=array()) { global $config;
return 'rebuild'; return 'rebuild';
case 'defer': case 'defer':
// Ok, it gets interesting here :) // Ok, it gets interesting here :)
get_queue('generate')->push(serialize(array('build', $fun, $array, $action))); $queue = Queues::get_queue($config, 'generate');
if ($queue === false) {
if ($config['syslog']) {
_syslog(LOG_ERR, "Could not initialize generate queue, falling back to immediate rebuild strategy");
}
return 'rebuild';
}
$ret = $queue->push(serialize(array('build', $fun, $array, $action)));
if ($ret === false) {
if ($config['syslog']) {
_syslog(LOG_ERR, "Could not push item in the queue, falling back to immediate rebuild strategy");
}
return 'rebuild';
}
return 'ignore'; return 'ignore';
case 'build_on_load': case 'build_on_load':
return 'delete'; return 'delete';

View File

@ -1,39 +1,84 @@
<?php <?php
class Lock {
function __construct($key) { global $config;
if ($config['lock']['enabled'] == 'fs') {
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
$this->f = fopen("tmp/locks/$key", "w"); class Locks {
} private static function filesystem(string $key): Lock|false {
} $key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
// Get a shared lock $fd = fopen("tmp/locks/$key", "w");
function get($nonblock = false) { global $config; if ($fd === false) {
if ($config['lock']['enabled'] == 'fs') { return false;
$wouldblock = false; }
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) return false;
}
return $this;
}
// Get an exclusive lock return new class($fd) implements Lock {
function get_ex($nonblock = false) { global $config; // Resources have no type in php.
if ($config['lock']['enabled'] == 'fs') { private mixed $f;
$wouldblock = false;
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) return false;
}
return $this;
}
// Free a lock
function free() { global $config; function __construct($fd) {
if ($config['lock']['enabled'] == 'fs') { $this->f = $fd;
flock($this->f, LOCK_UN); }
}
return $this; public function get(bool $nonblock = false): Lock|false {
} $wouldblock = false;
flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) {
return false;
}
return $this;
}
public function get_ex(bool $nonblock = false): Lock|false {
$wouldblock = false;
flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock);
if ($nonblock && $wouldblock) {
return false;
}
return $this;
}
public function free(): Lock {
flock($this->f, LOCK_UN);
return $this;
}
};
}
/**
* No-op. Can be used for mocking.
*/
public static function none(): Lock|false {
return new class() implements Lock {
public function get(bool $nonblock = false): Lock|false {
return $this;
}
public function get_ex(bool $nonblock = false): Lock|false {
return $this;
}
public function free(): Lock {
return $this;
}
};
}
public static function get_lock(array $config, string $key): Lock|false {
if ($config['lock']['enabled'] == 'fs') {
return self::filesystem($key);
} else {
return self::none();
}
}
}
interface Lock {
// Get a shared lock
public function get(bool $nonblock = false): Lock|false;
// Get an exclusive lock
public function get_ex(bool $nonblock = false): Lock|false;
// Free a lock
public function free(): Lock;
} }

View File

@ -1,49 +1,98 @@
<?php <?php
class Queue { class Queues {
function __construct($key) { global $config; private static $queues = array();
if ($config['queue']['enabled'] == 'fs') {
$this->lock = new Lock($key);
$key = str_replace('/', '::', $key);
$key = str_replace("\0", '', $key);
$this->key = "tmp/queue/$key/";
}
}
function push($str) { global $config;
if ($config['queue']['enabled'] == 'fs') {
$this->lock->get_ex();
file_put_contents($this->key.microtime(true), $str);
$this->lock->free();
}
return $this;
}
function pop($n = 1) { global $config; /**
if ($config['queue']['enabled'] == 'fs') { * This queue implementation isn't actually ordered, so it works more as a "bag".
$this->lock->get_ex(); */
$dir = opendir($this->key); private static function filesystem(string $key, Lock $lock): Queue {
$paths = array(); $key = str_replace('/', '::', $key);
while ($n > 0) { $key = str_replace("\0", '', $key);
$path = readdir($dir); $key = "tmp/queue/$key/";
if ($path === FALSE) break;
elseif ($path == '.' || $path == '..') continue; return new class($key, $lock) implements Queue {
else { $paths[] = $path; $n--; } private Lock $lock;
} private string $key;
$out = array();
foreach ($paths as $v) {
$out []= file_get_contents($this->key.$v); function __construct(string $key, Lock $lock) {
unlink($this->key.$v); $this->lock = $lock;
} $this->key = $key;
$this->lock->free(); }
return $out;
} public function push(string $str): bool {
} $this->lock->get_ex();
$ret = file_put_contents($this->key . microtime(true), $str);
$this->lock->free();
return $ret !== false;
}
public function pop(int $n = 1): array {
$this->lock->get_ex();
$dir = opendir($this->key);
$paths = array();
while ($n > 0) {
$path = readdir($dir);
if ($path === false) {
break;
} elseif ($path == '.' || $path == '..') {
continue;
} else {
$paths[] = $path;
$n--;
}
}
$out = array();
foreach ($paths as $v) {
$out[] = file_get_contents($this->key . $v);
unlink($this->key . $v);
}
$this->lock->free();
return $out;
}
};
}
/**
* No-op. Can be used for mocking.
*/
public static function none(): Queue {
return new class() implements Queue {
public function push(string $str): bool {
return true;
}
public function pop(int $n = 1): array {
return array();
}
};
}
public static function get_queue(array $config, string $name): Queue|false {
if (!isset(self::$queues[$name])) {
if ($config['queue']['enabled'] == 'fs') {
$lock = Locks::get_lock($config, $name);
if ($lock === false) {
return false;
}
self::$queues[$name] = self::filesystem($name, $lock);
} else {
self::$queues[$name] = self::none();
}
}
return self::$queues[$name];
}
} }
// Don't use the constructor. Use the get_queue function. interface Queue {
$queues = array(); // Push a string in the queue.
public function push(string $str): bool;
function get_queue($name) { global $queues; // Get a string from the queue.
return $queues[$name] = isset ($queues[$name]) ? $queues[$name] : new Queue($name); public function pop(int $n = 1): array;
} }