diff --git a/src/DbMapper.php b/src/DbMapper.php index 6ba3557..9172917 100644 --- a/src/DbMapper.php +++ b/src/DbMapper.php @@ -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 */ 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 { diff --git a/src/DbObject.php b/src/DbObject.php index 0821145..2f1b5b9 100644 --- a/src/DbObject.php +++ b/src/DbObject.php @@ -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 */ + 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); } } diff --git a/src/Internal/Driver/CacheContainer.php b/src/Internal/Containers/CacheContainer.php similarity index 97% rename from src/Internal/Driver/CacheContainer.php rename to src/Internal/Containers/CacheContainer.php index 1ad4d0d..82b735f 100644 --- a/src/Internal/Driver/CacheContainer.php +++ b/src/Internal/Containers/CacheContainer.php @@ -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 { diff --git a/src/Internal/Containers/ObjectContainer.php b/src/Internal/Containers/ObjectContainer.php new file mode 100644 index 0000000..de0aae2 --- /dev/null +++ b/src/Internal/Containers/ObjectContainer.php @@ -0,0 +1,177 @@ +. + * + * @author Daniil Gentili + * @author Alexander Pankratov + * @copyright 2016-2024 Daniil Gentili + * @copyright 2016-2024 Alexander Pankratov + * @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 + */ + private array $cache = []; + + private int $cacheTtl; + + /** + * Cache cleanup watcher ID. + */ + private ?string $cacheCleanupId = null; + + private LocalMutex $mutex; + + public function __construct( + /** @var DbArray */ + 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(...)); + } + } +} diff --git a/src/Internal/Containers/ObjectReference.php b/src/Internal/Containers/ObjectReference.php new file mode 100644 index 0000000..3cce5d9 --- /dev/null +++ b/src/Internal/Containers/ObjectReference.php @@ -0,0 +1,36 @@ +. + * + * @author Daniil Gentili + * @author Alexander Pankratov + * @copyright 2016-2024 Daniil Gentili + * @copyright 2016-2024 Alexander Pankratov + * @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); + } +} diff --git a/src/Internal/Driver/CachedArray.php b/src/Internal/Driver/CachedArray.php index 77b5dd3..f9fafad 100644 --- a/src/Internal/Driver/CachedArray.php +++ b/src/Internal/Driver/CachedArray.php @@ -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(); diff --git a/src/Internal/Driver/ObjectArray.php b/src/Internal/Driver/ObjectArray.php new file mode 100644 index 0000000..4d033bd --- /dev/null +++ b/src/Internal/Driver/ObjectArray.php @@ -0,0 +1,102 @@ +. + * + * @author Daniil Gentili + * @author Alexander Pankratov + * @copyright 2016-2024 Daniil Gentili + * @copyright 2016-2024 Alexander Pankratov + * @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 + */ +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(); + } +} diff --git a/src/OrmObject.php b/src/OrmObject.php deleted file mode 100644 index e69de29..0000000 diff --git a/src/ValueType.php b/src/ValueType.php index 6a9a482..1d75ef6 100644 --- a/src/ValueType.php +++ b/src/ValueType.php @@ -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'; }