This commit is contained in:
Daniil Gentili 2024-03-25 20:13:13 +01:00
parent 5c3086e266
commit a821954023
9 changed files with 374 additions and 39 deletions

View File

@ -16,9 +16,10 @@
namespace danog\AsyncOrm;
use Amp\ForbidCloning;
use Amp\ForbidSerialization;
use Amp\Sync\LocalKeyedMutex;
use AssertionError;
use WeakMap;
/**
* Async DB mapper.
@ -28,9 +29,12 @@ use WeakMap;
*/
final readonly class DbMapper
{
use ForbidCloning;
use ForbidSerialization;
/** @var DbArray<string|int, T> */
private readonly DbArray $arr;
private readonly LocalKeyedMutex $mutex;
private readonly WeakMap $inited;
/**
* Constructor.
*
@ -43,7 +47,7 @@ final readonly class DbMapper
* @param ?self $previous Previous instance, used for migrations.
*/
public function __construct(
private readonly string $table,
public readonly string $table,
private readonly string $class,
private readonly Settings $settings,
KeyType $keyType,
@ -54,7 +58,6 @@ final readonly class DbMapper
if (\is_subclass_of($class, DbArray::class)) {
throw new AssertionError("$class must extend DbArray!");
}
$this->inited = new WeakMap;
$this->mutex = new LocalKeyedMutex;
$config = new FieldConfig(
$table,
@ -65,12 +68,6 @@ final readonly class DbMapper
$optimizeIfWastedGtMb
);
$this->arr = $config->get($previous?->arr);
if ($previous !== null) {
foreach ($previous->inited as $key => $obj) {
$obj->__initDb($this->table, $this->settings);
$this->inited[$key] = $obj;
}
}
}
/**
@ -82,13 +79,12 @@ final readonly class DbMapper
{
$lock = $this->mutex->acquire((string) $key);
try {
if (isset($this->inited[$key])) {
if (isset($this->arr[$key])) {
throw new AssertionError("An object under the key $key already exists!");
}
$obj = new $this->class;
$obj->__initDb($this->table, $this->settings);
$this->arr[$key] = $obj;
$this->inited[$key] = $obj;
return $obj;
} finally {
$lock->release();
@ -106,13 +102,12 @@ final readonly class DbMapper
{
$lock = $this->mutex->acquire((string) $key);
try {
if (isset($this->inited[$key])) {
return $this->inited[$key];
}
$obj = $this->arr[$key];
if ($obj !== null) {
$obj->__initDb($this->table, $this->settings);
$this->inited[$key] = $obj;
$obj->__initDb(
$this->table,
$this->settings,
);
}
return $obj;
} finally {

View File

@ -19,6 +19,7 @@
namespace danog\AsyncOrm;
use danog\AsyncOrm\Annotations\OrmMappedArray;
use danog\AsyncOrm\Internal\Driver\CachedArray;
use danog\AsyncOrm\Settings\DriverSettings;
use danog\AsyncOrm\Settings\Mysql;
use ReflectionClass;
@ -28,13 +29,20 @@ use function Amp\Future\await;
abstract class DbObject
{
/** @var list<CachedArray> */
private array $properties;
private DbArray $mapper;
private string|int $key;
/**
* Initialize database instance.
*
* @internal
*/
final public function __initDb(string $table, Settings $settings): void
final public function initDb(DbArray $mapper, string|int $key, FieldConfig $config): void
{
$this->mapper = $mapper;
$this->key = $key;
$promises = [];
foreach ((new ReflectionClass(static::class))->getProperties() as $property) {
$attr = $property->getAttributes(OrmMappedArray::class);
@ -45,31 +53,43 @@ abstract class DbObject
$ttl = $attr->cacheTtl;
$optimize = $attr->optimizeIfWastedGtMb;
if ($settings instanceof DriverSettings) {
$ttl ??= $settings->cacheTtl;
if ($config->settings instanceof DriverSettings) {
$ttl ??= $config->settings->cacheTtl;
if ($settings instanceof Mysql) {
$optimize ??= $settings->optimizeIfWastedGtMb;
if ($config->settings instanceof Mysql) {
$optimize ??= $config->settings->optimizeIfWastedGtMb;
}
}
$config = new FieldConfig(
$table.'_'.$property->getName(),
$settings,
$config->table.'_'.$property->getName(),
$config->settings,
$attr->keyType,
$attr->valueType,
$ttl,
$optimize,
);
$promises[$property] = async(
$config->get(...),
$this->{$property} ?? null
);
$promises[] = async(function () use ($config, $property) {
$v = $config->get($property->getValue());
$property->setValue($v);
if ($v instanceof CachedArray) {
$this->properties []= $v;
}
$promises = await($promises);
foreach ($promises as $key => $data) {
$this->{$key} = $data;
});
}
await($promises);
}
/**
* Save object to database.
*/
public function save(): void
{
$promises = [async($this->mapper->set(...), $this->key, $this)];
foreach ($this->properties as $v) {
$promises []= async($v->flushCache(...));
}
await($promises);
}
}

View File

@ -16,7 +16,7 @@
* @link https://daniil.it/AsyncOrm AsyncOrm documentation
*/
namespace danog\AsyncOrm\Internal\Driver;
namespace danog\AsyncOrm\Internal\Containers;
use Amp\Sync\LocalMutex;
use danog\AsyncOrm\DbArray;
@ -51,8 +51,7 @@ final class CacheContainer
}
public function __sleep()
{
$this->flushCache();
return ['cache', 'ttl', 'inner'];
return ['inner'];
}
public function __wakeup(): void
{

View File

@ -0,0 +1,177 @@
<?php declare(strict_types=1);
/**
* This file is part of AsyncOrm.
* AsyncOrm is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General private License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* AsyncOrm 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 Affero General private License for more details.
* You should have received a copy of the GNU General private License along with AsyncOrm.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @author Alexander Pankratov <alexander@i-c-a.su>
* @copyright 2016-2024 Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2024 Alexander Pankratov <alexander@i-c-a.su>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://daniil.it/AsyncOrm AsyncOrm documentation
*/
namespace danog\AsyncOrm\Internal\Containers;
use Amp\Sync\LocalMutex;
use danog\AsyncOrm\DbArray;
use danog\AsyncOrm\DbObject;
use danog\AsyncOrm\FieldConfig;
use Revolt\EventLoop;
use Traversable;
/** @internal */
final class ObjectContainer
{
/**
* @var array<ObjectReference>
*/
private array $cache = [];
private int $cacheTtl;
/**
* Cache cleanup watcher ID.
*/
private ?string $cacheCleanupId = null;
private LocalMutex $mutex;
public function __construct(
/** @var DbArray<array-key, DbObject> */
public DbArray $inner,
public FieldConfig $config,
) {
$this->mutex = new LocalMutex;
}
public function __sleep()
{
return ['inner'];
}
public function __wakeup(): void
{
$this->mutex = new LocalMutex;
}
public function startCacheCleanupLoop(int $cacheTtl): void
{
$this->cacheTtl = $cacheTtl;
if ($this->cacheCleanupId) {
EventLoop::cancel($this->cacheCleanupId);
}
$this->cacheCleanupId = EventLoop::repeat(
\max(1, $this->cacheTtl / 5),
fn () => $this->flushCache(),
);
}
public function stopCacheCleanupLoop(): void
{
if ($this->cacheCleanupId) {
EventLoop::cancel($this->cacheCleanupId);
$this->cacheCleanupId = null;
}
}
public function get(string|int $index): mixed
{
if (isset($this->cache[$index])) {
$obj = $this->cache[$index];
$ref = $obj->reference->get();
if ($ref !== null) {
$obj->ttl = \time() + $this->cacheTtl;
return $obj;
}
unset($this->cache[$index]);
}
$result = $this->inner->offsetGet($index);
if (isset($this->cache[$index])) {
return $this->cache[$index]->reference->get();
}
\assert($result instanceof DbObject);
$result->initDb($this->inner, $index, $this->config);
$this->cache[$index] = new ObjectReference($result, \time() + $this->cacheTtl);
return $result;
}
public function set(string|int $key, DbObject $value): void
{
if (isset($this->cache[$key]) && $this->cache[$key]->reference->get() === $value) {
return;
}
$value->initDb($this->inner, $key, $this->config);
$this->cache[$key] = new ObjectReference($value, \time() + $this->cacheTtl);
$this->inner->set($key, $value);
}
public function unset(string|int $key): void
{
unset($this->cache[$key]);
$this->inner->unset($key);
}
public function getIterator(): Traversable
{
$this->flushCache();
foreach ($this->inner->getIterator() as $key => $value) {
if (isset($this->cache[$key])) {
$obj = $this->cache[$key];
$ref = $obj->reference->get();
if ($ref !== null) {
$obj->ttl = \time() + $this->cacheTtl;
yield $obj;
continue;
}
}
$value->initDb($this->inner, $key, $this->config);
$this->cache[$key] = new ObjectReference($value, \time() + $this->cacheTtl);
yield $value;
}
}
public function count(): int
{
$this->flushCache();
return $this->inner->count();
}
public function clear(): void
{
$lock = $this->mutex->acquire();
$this->cache = [];
$lock->release();
$this->inner->clear();
}
/**
* Flush all flushable keys.
*/
public function flushCache(): void
{
$lock = $this->mutex->acquire();
try {
$now = \time();
$new = [];
foreach ($this->cache as $key => $value) {
if ($value->ttl > $now) {
$value->obj = null;
}
if ($value->reference->get() !== null) {
$new[$key] = $value;
}
}
$this->cache = $new;
} finally {
EventLoop::queue($lock->release(...));
}
}
}

View File

@ -0,0 +1,36 @@
<?php declare(strict_types=1);
/**
* This file is part of AsyncOrm.
* AsyncOrm is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General private License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* AsyncOrm 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 Affero General private License for more details.
* You should have received a copy of the GNU General private License along with AsyncOrm.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @author Alexander Pankratov <alexander@i-c-a.su>
* @copyright 2016-2024 Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2024 Alexander Pankratov <alexander@i-c-a.su>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://daniil.it/AsyncOrm AsyncOrm documentation
*/
namespace danog\AsyncOrm\Internal\Containers;
use danog\AsyncOrm\DbObject;
use WeakReference;
/** @internal */
final class ObjectReference
{
public readonly WeakReference $reference;
public ?DbObject $obj;
public function __construct(
DbObject $object,
public int $ttl
) {
$this->obj = $object;
$this->reference = WeakReference::create($object);
}
}

View File

@ -21,6 +21,7 @@ namespace danog\AsyncOrm\Internal\Driver;
use danog\AsyncOrm\DbArray;
use danog\AsyncOrm\Driver\MemoryArray;
use danog\AsyncOrm\FieldConfig;
use danog\AsyncOrm\Internal\Containers\CacheContainer;
use Traversable;
/**
@ -54,7 +55,7 @@ final class CachedArray extends DbArray
$previous->cache->flushCache();
return $previous->cache->inner;
}
$previous->cache->startCacheCleanupLoop($config->annotation->cacheTtl);
$previous->cache->startCacheCleanupLoop($config->cacheTtl);
return $previous;
}
@ -68,6 +69,11 @@ final class CachedArray extends DbArray
$this->cache->stopCacheCleanupLoop();
}
public function flushCache(): void
{
$this->cache->flushCache();
}
public function count(): int
{
return $this->cache->count();

View File

@ -0,0 +1,102 @@
<?php declare(strict_types=1);
/**
* This file is part of AsyncOrm.
* AsyncOrm is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
* AsyncOrm 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 Affero General Public License for more details.
* You should have received a copy of the GNU General Public License along with AsyncOrm.
* If not, see <http://www.gnu.org/licenses/>.
*
* @author Daniil Gentili <daniil@daniil.it>
* @author Alexander Pankratov <alexander@i-c-a.su>
* @copyright 2016-2024 Daniil Gentili <daniil@daniil.it>
* @copyright 2016-2024 Alexander Pankratov <alexander@i-c-a.su>
* @license https://opensource.org/licenses/AGPL-3.0 AGPLv3
* @link https://daniil.it/AsyncOrm AsyncOrm documentation
*/
namespace danog\AsyncOrm\Internal\Driver;
use danog\AsyncOrm\DbArray;
use danog\AsyncOrm\Driver\MemoryArray;
use danog\AsyncOrm\FieldConfig;
use danog\AsyncOrm\Internal\Containers\ObjectContainer;
use Traversable;
/**
* Object caching proxy.
*
* @internal
*
* @template TKey as array-key
* @template TValue
*
* @extends DbArray<TKey, TValue>
*/
final class ObjectArray extends DbArray
{
private readonly ObjectContainer $cache;
/**
* Get instance.
*/
public static function getInstance(FieldConfig $config, DbArray|null $previous): DbArray
{
$new = $config->settings->getDriverClass();
if ($previous === null) {
$previous = new self($new::getInstance($config, null), $config);
} elseif ($previous instanceof self) {
$previous->cache->inner = $new::getInstance($config, $previous->cache->inner);
$previous->cache->config = $config;
} else {
$previous = new self($new::getInstance($config, $previous), $config);
}
if ($previous->cache->inner instanceof MemoryArray) {
$previous->cache->flushCache();
return $previous->cache->inner;
}
$previous->cache->startCacheCleanupLoop($config->cacheTtl);
return $previous;
}
public function __construct(DbArray $inner, FieldConfig $config)
{
$this->cache = new ObjectContainer($inner, $config);
}
public function __destruct()
{
$this->cache->stopCacheCleanupLoop();
}
public function count(): int
{
return $this->cache->count();
}
public function clear(): void
{
$this->cache->clear();
}
public function get(mixed $index): mixed
{
return $this->cache->get($index);
}
public function set(string|int $key, mixed $value): void
{
$this->cache->set($key, $value);
}
public function unset(string|int $key): void
{
$this->cache->unset($key);
}
public function getIterator(): Traversable
{
return $this->cache->getIterator();
}
}

View File

View File

@ -30,13 +30,13 @@ enum ValueType: string
*/
case INT = 'int';
/**
* Objects, serialized as specified in the settings.
* Objects extending DbObject, serialized as specified in the settings.
*/
case OBJECT = 'object';
/**
* Values of any type, serialized as specified in the settings.
* Values of any scalar type, serialized as specified in the settings.
*
* Using MIXED worsens performances, please use STRING, INT or OBJECT whenever possible.
* Using SCALAR worsens performances, please use STRING, INT or OBJECT whenever possible.
*/
case MIXED = 'object';
case SCALAR = 'scalar';
}