From aa7a8a49cf5cc99bd03a400b9c4d0a0debc30276 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Mon, 30 May 2022 20:26:40 -0100 Subject: [PATCH] async/split process Signed-off-by: Maxence Lange --- appinfo/routes.php | 3 +- lib/CircleSharesManager.php | 10 +- lib/Controller/EventWrapperController.php | 46 +++- lib/Controller/SyncController.php | 8 +- lib/Db/CoreQueryBuilder.php | 54 ++-- lib/Db/CoreRequestBuilder.php | 2 + lib/Db/EventWrapperRequest.php | 36 ++- lib/Db/SyncedItemLockRequest.php | 48 +++- .../FederatedSyncConflictException.php | 22 +- lib/Exceptions/FederatedSyncException.php | 96 +++++++ .../FederatedSyncPermissionException.php | 47 ++++ lib/Exceptions/InternalAsyncException.php | 37 +++ lib/Exceptions/SyncedItemLockException.php | 47 ++++ .../SyncedItemNotFoundException.php | 15 +- .../SyncedShareNotFoundException.php | 13 + .../SyncedSharedAlreadyExistException.php | 15 +- lib/ICircleSharesManager.php | 8 +- lib/IFederatedSyncManager.php | 31 ++- lib/IInternalAsync.php | 42 +++ lib/InternalAsync/AsyncItemUpdate.php | 52 ++++ lib/InternalAsync/AsyncTest.php | 63 +++++ .../Version0025Date20220510104622.php | 36 ++- lib/Model/Federated/EventWrapper.php | 138 ++++++++-- lib/Model/FederatedUser.php | 12 +- lib/Model/SyncedItemLock.php | 48 ++-- lib/Model/SyncedWrapper.php | 29 ++ lib/Service/AsyncService.php | 254 ++++++++++++++++++ lib/Service/EventWrapperService.php | 60 ++++- lib/Service/FederatedEventService.php | 50 +--- lib/Service/FederatedSyncItemService.php | 210 +++++++++++---- lib/Service/GSUpstreamService.php | 4 +- lib/Service/RemoteUpstreamService.php | 16 +- lib/Tools/Model/ReferencedDataStore.php | 12 +- lib/Tools/Traits/TAsync.php | 4 +- 34 files changed, 1324 insertions(+), 244 deletions(-) create mode 100644 lib/Exceptions/FederatedSyncException.php create mode 100644 lib/Exceptions/FederatedSyncPermissionException.php create mode 100644 lib/Exceptions/InternalAsyncException.php create mode 100644 lib/Exceptions/SyncedItemLockException.php create mode 100644 lib/IInternalAsync.php create mode 100644 lib/InternalAsync/AsyncItemUpdate.php create mode 100644 lib/InternalAsync/AsyncTest.php create mode 100644 lib/Service/AsyncService.php diff --git a/appinfo/routes.php b/appinfo/routes.php index 2ce2639a0..b113fa424 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -101,7 +101,8 @@ ], 'routes' => [ - ['name' => 'EventWrapper#asyncBroadcast', 'url' => '/async/{token}/', 'verb' => 'POST'], + ['name' => 'EventWrapper#asyncBroadcast', 'url' => '/async/broadcast/{token}', 'verb' => 'POST'], + ['name' => 'EventWrapper#asyncInternal', 'url' => '/async/internal/{token}', 'verb' => 'POST'], ['name' => 'Remote#appService', 'url' => '/', 'verb' => 'GET'], ['name' => 'Remote#test', 'url' => '/test', 'verb' => 'GET'], diff --git a/lib/CircleSharesManager.php b/lib/CircleSharesManager.php index f5729e53f..d419f6a82 100644 --- a/lib/CircleSharesManager.php +++ b/lib/CircleSharesManager.php @@ -34,6 +34,7 @@ use Exception; use OCA\Circles\Exceptions\CircleSharesManagerException; use OCA\Circles\Model\Probes\CircleProbe; +use OCA\Circles\Model\SyncedItemLock; use OCA\Circles\Service\CircleService; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\DebugService; @@ -214,7 +215,10 @@ public function deleteShare(string $itemId, string $circleId): void { */ public function updateItem( string $itemId, - array $extraData = [] + string $updateType = '', + string $updateTypeId = '', + array $extraData = [], + bool $sumCheck = true ): void { $this->mustHaveOrigin(); @@ -226,12 +230,13 @@ public function updateItem( 'appId' => $this->originAppId, 'itemType' => $this->originItemType, 'itemId' => $itemId, + 'updateType' => $updateType, + 'updateTypeId' => $updateTypeId, 'extraData' => $extraData ] ); try { -// $this->mustHaveOrigin(); // // TODO: verify rules that apply when sharing to a circle // $probe = new CircleProbe(); @@ -260,6 +265,7 @@ public function updateItem( $this->federatedSyncItemService->requestSyncedItemUpdate( $this->federatedUserService->getCurrentEntity(), $syncedItem, + new SyncedItemLock($updateType, $updateTypeId, $sumCheck), $extraData ); } catch (Exception $e) { diff --git a/lib/Controller/EventWrapperController.php b/lib/Controller/EventWrapperController.php index fd2ca5a84..9bae8731b 100644 --- a/lib/Controller/EventWrapperController.php +++ b/lib/Controller/EventWrapperController.php @@ -32,6 +32,8 @@ namespace OCA\Circles\Controller; use OCA\Circles\AppInfo\Application; +use OCA\Circles\Exceptions\EventWrapperNotFoundException; +use OCA\Circles\Service\AsyncService; use OCA\Circles\Service\ConfigService; use OCA\Circles\Service\EventWrapperService; use OCA\Circles\Service\FederatedEventService; @@ -66,6 +68,8 @@ class EventWrapperController extends Controller { /** @var RemoteDownstreamService */ private $remoteDownstreamService; + private AsyncService $asyncService; + /** @var ConfigService */ private $configService; @@ -79,6 +83,7 @@ class EventWrapperController extends Controller { * @param FederatedEventService $federatedEventService * @param RemoteUpstreamService $remoteUpstreamService * @param RemoteDownstreamService $remoteDownstreamService + * @param AsyncService $asyncService * @param ConfigService $configService */ public function __construct( @@ -88,6 +93,7 @@ public function __construct( FederatedEventService $federatedEventService, RemoteUpstreamService $remoteUpstreamService, RemoteDownstreamService $remoteDownstreamService, + AsyncService $asyncService, ConfigService $configService ) { parent::__construct($appName, $request); @@ -95,6 +101,7 @@ public function __construct( $this->federatedEventService = $federatedEventService; $this->remoteUpstreamService = $remoteUpstreamService; $this->remoteDownstreamService = $remoteDownstreamService; + $this->asyncService = $asyncService; $this->configService = $configService; $this->setup('app', Application::APP_ID); @@ -105,7 +112,7 @@ public function __construct( /** * Called locally. * - * Async process and broadcast the event to every instances of GS + * Async process and broadcast the event to every instance of GS * This should be initiated by the instance that owns the Circles. * * @PublicPage @@ -116,19 +123,15 @@ public function __construct( * @return DataResponse */ public function asyncBroadcast(string $token): DataResponse { - $wrappers = $this->remoteUpstreamService->getEventsByToken($token); + $wrappers = $this->eventWrapperService->getBroadcastByToken($token); if (empty($wrappers) && $token !== 'test-dummy-token') { return new DataResponse([], Http::STATUS_OK); } // closing socket, keep current process running. - $this->async(); - - foreach ($wrappers as $wrapper) { - $this->eventWrapperService->manageWrapper($wrapper); - } - - $this->eventWrapperService->confirmStatus($token); + $this->asyncService->setSplittable(true); + $this->asyncService->split(); + $this->eventWrapperService->performBroadcast($token, $wrappers); // so circles:check can check async is fine if ($token === 'test-dummy-token') { @@ -140,6 +143,31 @@ public function asyncBroadcast(string $token): DataResponse { } + /** + * Called locally. + * + * Async process and continue using IInternalAsync + * + * @PublicPage + * @NoCSRFRequired + * + * @param string $token + * + * @return DataResponse + * @throws EventWrapperNotFoundException + */ + public function asyncInternal(string $token): DataResponse { + $this->asyncService->setSplittable(true); + $this->asyncService->split(); + + $this->eventWrapperService->performInternal($token); + + // exit() or useless log will be generated + exit(); + } + + + // /** // * Status Event. This is an event to check status of items between instances. // * diff --git a/lib/Controller/SyncController.php b/lib/Controller/SyncController.php index 37b6f1a55..e8233e67d 100644 --- a/lib/Controller/SyncController.php +++ b/lib/Controller/SyncController.php @@ -162,13 +162,15 @@ public function updateSyncedItem(): DataResponse { ] ); - $updated = $this->federatedSyncItemService->requestSyncedItemUpdate( + $this->federatedSyncItemService->requestSyncedItemUpdate( $wrapper->getFederatedUser(), $local, - $wrapper->getExtraData() + $wrapper->getLock(), + $wrapper->getExtraData(), + true ); - return new DataResponse($updated); + return new DataResponse([]); } catch (Exception $e) { $this->e($e); diff --git a/lib/Db/CoreQueryBuilder.php b/lib/Db/CoreQueryBuilder.php index 5297f0601..bd1b803ac 100644 --- a/lib/Db/CoreQueryBuilder.php +++ b/lib/Db/CoreQueryBuilder.php @@ -56,33 +56,33 @@ class CoreQueryBuilder extends ExtendedQueryBuilder { use TArrayTools; - public const SINGLE = 'cs'; - public const CIRCLE = 'cc'; - public const MEMBER = 'mm'; - public const OWNER = 'wn'; - public const FEDERATED_EVENT = 'ev'; - public const REMOTE = 'rm'; - public const BASED_ON = 'on'; - public const INITIATOR = 'in'; - public const DIRECT_INITIATOR = 'di'; - public const MEMBERSHIPS = 'ms'; - public const CONFIG = 'cf'; - public const UPSTREAM_MEMBERSHIPS = 'up'; - public const INHERITANCE_FROM = 'ih'; - public const INHERITED_BY = 'by'; - public const INVITED_BY = 'nv'; - public const MOUNT = 'mo'; - public const MOUNTPOINT = 'mp'; - public const SHARE = 'sh'; - public const FILE_CACHE = 'fc'; - public const STORAGES = 'st'; - public const TOKEN = 'tk'; - public const OPTIONS = 'pt'; - public const HELPER = 'hp'; - public const SYNC_ITEM = 'si'; - public const SYNC_SHARE = 'ss'; - public const SYNC_LOCK = 'sl'; - public const DEBUG = 'bg'; + public const SINGLE = 'ca'; + public const CIRCLE = 'cb'; + public const MEMBER = 'cc'; + public const OWNER = 'cd'; + public const FEDERATED_EVENT = 'ce'; + public const REMOTE = 'cf'; + public const BASED_ON = 'cg'; + public const INITIATOR = 'ch'; + public const DIRECT_INITIATOR = 'ci'; + public const MEMBERSHIPS = 'cj'; + public const CONFIG = 'ck'; + public const UPSTREAM_MEMBERSHIPS = 'cl'; + public const INHERITANCE_FROM = 'cm'; + public const INHERITED_BY = 'cn'; + public const INVITED_BY = 'co'; + public const MOUNT = 'cp'; + public const MOUNTPOINT = 'cq'; + public const SHARE = 'cr'; + public const FILE_CACHE = 'cs'; + public const STORAGES = 'ct'; + public const TOKEN = 'cu'; + public const OPTIONS = 'cv'; + public const HELPER = 'cw'; + public const SYNC_ITEM = 'cx'; + public const SYNC_SHARE = 'cy'; + public const SYNC_LOCK = 'cz'; + public const DEBUG = 'c0'; public static $SQL_PATH = [ diff --git a/lib/Db/CoreRequestBuilder.php b/lib/Db/CoreRequestBuilder.php index 6101dbc1c..5df86bfa1 100644 --- a/lib/Db/CoreRequestBuilder.php +++ b/lib/Db/CoreRequestBuilder.php @@ -124,6 +124,8 @@ class CoreRequestBuilder { self::TABLE_EVENT => [ 'token', 'event', + 'store', + 'event_type', 'result', 'instance', 'interface', diff --git a/lib/Db/EventWrapperRequest.php b/lib/Db/EventWrapperRequest.php index c1d92789f..737e6efad 100644 --- a/lib/Db/EventWrapperRequest.php +++ b/lib/Db/EventWrapperRequest.php @@ -31,6 +31,7 @@ namespace OCA\Circles\Db; +use OCA\Circles\Exceptions\EventWrapperNotFoundException; use OCA\Circles\Model\Federated\EventWrapper; /** @@ -43,15 +44,16 @@ class EventWrapperRequest extends EventWrapperRequestBuilder { /** * @param EventWrapper $wrapper + * + * @throws \OCP\DB\Exception */ public function save(EventWrapper $wrapper): void { $qb = $this->getEventWrapperInsertSql(); $qb->setValue('token', $qb->createNamedParameter($wrapper->getToken())) + ->setValue('event_type', $qb->createNamedParameter($wrapper->getEventType())) ->setValue( - 'event', $qb->createNamedParameter(json_encode($wrapper->getEvent(), JSON_UNESCAPED_SLASHES)) - ) - ->setValue( - 'result', $qb->createNamedParameter(json_encode($wrapper->getResult(), JSON_UNESCAPED_SLASHES)) + 'result', + $qb->createNamedParameter(json_encode($wrapper->getResult(), JSON_UNESCAPED_SLASHES)) ) ->setValue('instance', $qb->createNamedParameter($wrapper->getInstance())) ->setValue('interface', $qb->createNamedParameter($wrapper->getInterface())) @@ -60,7 +62,13 @@ public function save(EventWrapper $wrapper): void { ->setValue('status', $qb->createNamedParameter($wrapper->getStatus())) ->setValue('creation', $qb->createNamedParameter($wrapper->getCreation())); - $qb->execute(); + $event = ($wrapper->hasEvent()) ? json_encode($wrapper->getEvent(), JSON_UNESCAPED_SLASHES) : ''; + $qb->setValue('event', $qb->createNamedParameter($event)); + + $store = ($wrapper->hasStore()) ? json_encode($wrapper->getStore(), JSON_UNESCAPED_SLASHES) : ''; + $qb->setValue('store', $qb->createNamedParameter($store)); + + $qb->executeStatement(); } /** @@ -113,10 +121,26 @@ public function getFailedEvents(array $retryRange): array { * * @return EventWrapper[] */ - public function getByToken(string $token): array { + public function getBroadcastByToken(string $token): array { $qb = $this->getEventWrapperSelectSql(); $qb->limitToToken($token); + $qb->limit('event_type', EventWrapper::TYPE_BROADCAST); return $this->getItemsFromRequest($qb); } + + + /** + * @param string $token + * + * @return EventWrapper + * @throws EventWrapperNotFoundException + */ + public function getInternalByToken(string $token): EventWrapper { + $qb = $this->getEventWrapperSelectSql(); + $qb->limitToToken($token); + $qb->limit('event_type', EventWrapper::TYPE_INTERNAL); + + return $this->getItemFromRequest($qb); + } } diff --git a/lib/Db/SyncedItemLockRequest.php b/lib/Db/SyncedItemLockRequest.php index 19a8ae595..8918407f7 100644 --- a/lib/Db/SyncedItemLockRequest.php +++ b/lib/Db/SyncedItemLockRequest.php @@ -32,7 +32,9 @@ namespace OCA\Circles\Db; use OCA\Circles\Exceptions\InvalidIdException; +use OCA\Circles\Exceptions\SyncedItemNotFoundException; use OCA\Circles\Model\SyncedItemLock; +use OCA\Circles\Tools\Exceptions\InvalidItemException; /** * Class SyncedItemLockRequest @@ -51,11 +53,51 @@ public function save(SyncedItemLock $lock): void { $this->confirmValidIds([$lock->getSingleId()]); $qb = $this->getSyncedItemLockInsertSql(); - $qb->setValue('single_id', $qb->createNamedParameter($lock->getSingleId())) - ->setValue('update_type', $qb->createNamedParameter($lock->getUpdateType())) + $qb->setValue('update_type', $qb->createNamedParameter($lock->getUpdateType())) ->setValue('update_type_id', $qb->createNamedParameter($lock->getUpdateTypeId())) - ->setValue('time', $qb->createNamedParameter($lock->getTime())); + ->setValue('time', $qb->createNamedParameter(time())); $qb->execute(); } + + + /** + * @param SyncedItemLock $syncedLock + */ + public function remove(SyncedItemLock $syncedLock): void { + $qb = $this->getSyncedItemLockDeleteSql(); + + $qb->limit('update_type', $syncedLock->getUpdateType()); + $qb->limit('update_type_id', $syncedLock->getUpdateTypeId()); + + $qb->executeStatement(); + } + + /** + * @param SyncedItemLock $syncedLock + * + * @return SyncedItemLock + * @throws SyncedItemNotFoundException + * @throws InvalidItemException + */ + public function getSyncedItemLock(SyncedItemLock $syncedLock): SyncedItemLock { + $qb = $this->getSyncedItemLockSelectSql(); + + $qb->limit('update_type', $syncedLock->getUpdateType()); + $qb->limit('update_type_id', $syncedLock->getUpdateTypeId()); + + return $this->getItemFromRequest($qb); + } + + + /** + * @param int $time + */ + public function clean(int $time = 10): void { + $qb = $this->getSyncedItemLockSelectSql(); + $qb->lt('time', (time() - $time)); + + $qb->executeStatement(); + } + } diff --git a/lib/Exceptions/FederatedSyncConflictException.php b/lib/Exceptions/FederatedSyncConflictException.php index cfac9e77a..75bee9336 100644 --- a/lib/Exceptions/FederatedSyncConflictException.php +++ b/lib/Exceptions/FederatedSyncConflictException.php @@ -30,7 +30,25 @@ namespace OCA\Circles\Exceptions; -use Exception; +use OCP\AppFramework\Http; +use Throwable; -class FederatedSyncConflictException extends FederatedItemConflictException { +class FederatedSyncConflictException extends FederatedSyncException { + public const STATUS = Http::STATUS_CONFLICT; + + /** + * FederatedItemConflictException constructor. + * + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct($message, ($code > 0) ? $code : self::STATUS, $previous); + $this->setStatus(self::STATUS); + } } diff --git a/lib/Exceptions/FederatedSyncException.php b/lib/Exceptions/FederatedSyncException.php new file mode 100644 index 000000000..149821aac --- /dev/null +++ b/lib/Exceptions/FederatedSyncException.php @@ -0,0 +1,96 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Exceptions; + +use Exception; +use JsonSerializable; +use OCP\AppFramework\Http; +use Throwable; + +/** + * Class FederatedItemException + * + * @package OCA\Circles\Exceptions + */ +class FederatedSyncException extends Exception implements JsonSerializable { + public static array $CHILDREN = [ + SyncedItemNotFoundException::class, + SyncedItemLockException::class, + SyncedShareNotFoundException::class, + SyncedSharedAlreadyExistException::class + ]; + + + /** @var int */ + private int $status = Http::STATUS_BAD_REQUEST; + + + /** + * FederatedItemException constructor. + * + * @param string $message + * @param int $code + * @param Throwable|null $previous + */ + public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) { + parent::__construct($message, ($code > 0) ? $code : $this->status, $previous); + } + + + /** + * @param int $status + */ + protected function setStatus(int $status): void { + $this->status = $status; + } + + /** + * @return int + */ + public function getStatus(): int { + return $this->status; + } + + + /** + * @return array + */ + public function jsonSerialize(): array { + return [ + 'class' => get_class($this), + 'status' => $this->getStatus(), + 'code' => $this->getCode(), + 'message' => $this->getMessage() + ]; + } +} diff --git a/lib/Exceptions/FederatedSyncPermissionException.php b/lib/Exceptions/FederatedSyncPermissionException.php new file mode 100644 index 000000000..7ceb96869 --- /dev/null +++ b/lib/Exceptions/FederatedSyncPermissionException.php @@ -0,0 +1,47 @@ + + * @copyright 2021 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Exceptions; + +use OCP\AppFramework\Http; +use Throwable; + +class FederatedSyncPermissionException extends FederatedSyncException { + public const STATUS = Http::STATUS_METHOD_NOT_ALLOWED; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct($message, ($code > 0) ? $code : self::STATUS, $previous); + $this->setStatus(self::STATUS); + } +} diff --git a/lib/Exceptions/InternalAsyncException.php b/lib/Exceptions/InternalAsyncException.php new file mode 100644 index 000000000..2f12f0012 --- /dev/null +++ b/lib/Exceptions/InternalAsyncException.php @@ -0,0 +1,37 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Exceptions; + +use Exception; + +class InternalAsyncException extends Exception { +} diff --git a/lib/Exceptions/SyncedItemLockException.php b/lib/Exceptions/SyncedItemLockException.php new file mode 100644 index 000000000..bd7262985 --- /dev/null +++ b/lib/Exceptions/SyncedItemLockException.php @@ -0,0 +1,47 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\Circles\Exceptions; + +use OCP\AppFramework\Http; +use Throwable; + +class SyncedItemLockException extends FederatedSyncException { + public const STATUS = Http::STATUS_LOCKED; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct($message, ($code > 0) ? $code : self::STATUS, $previous); + $this->setStatus(self::STATUS); + } +} diff --git a/lib/Exceptions/SyncedItemNotFoundException.php b/lib/Exceptions/SyncedItemNotFoundException.php index 537fd309b..280d77ea4 100644 --- a/lib/Exceptions/SyncedItemNotFoundException.php +++ b/lib/Exceptions/SyncedItemNotFoundException.php @@ -30,5 +30,18 @@ namespace OCA\Circles\Exceptions; -class SyncedItemNotFoundException extends FederatedItemNotFoundException { +use OCP\AppFramework\Http; +use Throwable; + +class SyncedItemNotFoundException extends FederatedSyncException { + public const STATUS = Http::STATUS_NOT_FOUND; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct($message, ($code > 0) ? $code : self::STATUS, $previous); + $this->setStatus(self::STATUS); + } } diff --git a/lib/Exceptions/SyncedShareNotFoundException.php b/lib/Exceptions/SyncedShareNotFoundException.php index 10f27377d..1d45233f4 100644 --- a/lib/Exceptions/SyncedShareNotFoundException.php +++ b/lib/Exceptions/SyncedShareNotFoundException.php @@ -30,5 +30,18 @@ namespace OCA\Circles\Exceptions; +use OCP\AppFramework\Http; +use Throwable; + class SyncedShareNotFoundException extends FederatedItemNotFoundException { + public const STATUS = Http::STATUS_NOT_FOUND; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct($message, ($code > 0) ? $code : self::STATUS, $previous); + $this->setStatus(self::STATUS); + } } diff --git a/lib/Exceptions/SyncedSharedAlreadyExistException.php b/lib/Exceptions/SyncedSharedAlreadyExistException.php index 747f8c58f..a8ead52bf 100644 --- a/lib/Exceptions/SyncedSharedAlreadyExistException.php +++ b/lib/Exceptions/SyncedSharedAlreadyExistException.php @@ -30,7 +30,18 @@ namespace OCA\Circles\Exceptions; -use Exception; +use OCP\AppFramework\Http; +use Throwable; -class SyncedSharedAlreadyExistException extends Exception { +class SyncedSharedAlreadyExistException extends FederatedSyncException { + public const STATUS = Http::STATUS_BAD_REQUEST; + + public function __construct( + string $message = '', + int $code = 0, + ?Throwable $previous = null + ) { + parent::__construct($message, ($code > 0) ? $code : self::STATUS, $previous); + $this->setStatus(self::STATUS); + } } diff --git a/lib/ICircleSharesManager.php b/lib/ICircleSharesManager.php index 2785343a9..97ce19cfb 100644 --- a/lib/ICircleSharesManager.php +++ b/lib/ICircleSharesManager.php @@ -90,7 +90,13 @@ public function deleteShare(string $itemId, string $circleId): void; * @param string $itemId * @param array $extraData */ - public function updateItem(string $itemId, array $extraData): void; + public function updateItem( + string $itemId, + string $updateType, + string $updateTypeId, + array $extraData, + bool $sumCheck + ): void; /** * Initiate the deletion of an Item diff --git a/lib/IFederatedSyncManager.php b/lib/IFederatedSyncManager.php index cd268771c..725421b97 100644 --- a/lib/IFederatedSyncManager.php +++ b/lib/IFederatedSyncManager.php @@ -31,7 +31,6 @@ namespace OCA\Circles; -use JsonSerializable; use OCA\Circles\Exceptions\SyncedItemNotFoundException; use OCA\Circles\Model\FederatedUser; @@ -282,15 +281,39 @@ public function onShareDeletion( * Method is only called on the instance that owns the shared item * * @param string $itemId + * @param string $updateType + * @param string $updateTypeId * @param array $extraData * @param FederatedUser $federatedUser * - * @return array + * @return bool + * // TODO: define Exception that can be thrown by app: + * // - itemnotfound + * // - ItemUpdateException */ - public function isItemUpdatable( + public function isItemModifiable( string $itemId, + string $updateType, + string $updateTypeId, array $extraData, IFederatedUser $federatedUser - ): array; + ): bool; + + + /** + * @param string $itemId + * @param string $updateType + * @param string $updateTypeId + * @param array $extraData + * @param FederatedUser $federatedUser + */ + public function onItemModification( + string $itemId, + string $updateType, + string $updateTypeId, + array $extraData, + IFederatedUser $federatedUser + ): void; + } diff --git a/lib/IInternalAsync.php b/lib/IInternalAsync.php new file mode 100644 index 000000000..51c70aba2 --- /dev/null +++ b/lib/IInternalAsync.php @@ -0,0 +1,42 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles; + +use OCA\Circles\Tools\Model\ReferencedDataStore; + +interface IInternalAsync { + + public const STORE_INTERNAL_ASYNC = '_internalAsync'; + + public function runAsynced(ReferencedDataStore $store): void; + +} diff --git a/lib/InternalAsync/AsyncItemUpdate.php b/lib/InternalAsync/AsyncItemUpdate.php new file mode 100644 index 000000000..809fd8610 --- /dev/null +++ b/lib/InternalAsync/AsyncItemUpdate.php @@ -0,0 +1,52 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\InternalAsync; + +use OCA\Circles\IInternalAsync; +use OCA\Circles\Tools\Model\ReferencedDataStore; + + +class AsyncItemUpdate implements IInternalAsync { + + + public function __construct() { + } + + + public function runAsynced(ReferencedDataStore $store): void { + // update Checksum + + // broadcast update + + } + +} diff --git a/lib/InternalAsync/AsyncTest.php b/lib/InternalAsync/AsyncTest.php new file mode 100644 index 000000000..2c577bca8 --- /dev/null +++ b/lib/InternalAsync/AsyncTest.php @@ -0,0 +1,63 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\InternalAsync; + +use OCA\Circles\IInternalAsync; +use OCA\Circles\Service\AsyncService; +use OCA\Circles\Tools\Model\ReferencedDataStore; + + +class AsyncTest implements IInternalAsync { + + + private AsyncService $asyncService; + + public function __construct(AsyncService $asyncService) { + $this->asyncService = $asyncService; + } + + + public function runAsynced(ReferencedDataStore $store): void { + + \OC::$server->getLogger()->log(3, '-runAsynced ' . json_encode($store)); + $this->asyncService->asyncInternal( + AsyncTest::class, + new ReferencedDataStore( + [ + 'action' => 'test', + 'federatedUser' => $store->gObj('federatedUser') + ] + ) + ); + } + +} diff --git a/lib/Migration/Version0025Date20220510104622.php b/lib/Migration/Version0025Date20220510104622.php index f68192939..9cb7db5cf 100644 --- a/lib/Migration/Version0025Date20220510104622.php +++ b/lib/Migration/Version0025Date20220510104622.php @@ -61,6 +61,28 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); + if ($schema->hasTable('circles_event')) { + $table = $schema->getTable('circles_event'); + if (!$table->hasColumn('event_type')) { + $table->addColumn( + 'event_type', Types::STRING, [ + 'notnull' => false, + 'default' => 'broadcast', + 'length' => 15 + ] + ); + $table->addIndex(['event_type']); + } + if (!$table->hasColumn('store')) { + $table->addColumn( + 'store', Types::TEXT, [ + 'notnull' => false, + 'default' => '' + ] + ); + } + } + if (!$schema->hasTable('circles_item')) { $table = $schema->createTable('circles_item'); $table->addColumn( @@ -156,12 +178,12 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt 'unsigned' => true, ] ); - $table->addColumn( - 'single_id', Types::STRING, [ - 'notnull' => false, - 'length' => 31, - ] - ); +// $table->addColumn( +// 'single_id', Types::STRING, [ +// 'notnull' => false, +// 'length' => 31, +// ] +// ); $table->addColumn( 'update_type', Types::STRING, [ 'notnull' => false, @@ -183,7 +205,7 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ); $table->setPrimaryKey(['id']); - $table->addUniqueIndex(['single_id', 'update_type', 'update_type_id'], 'c_siututi'); + $table->addUniqueIndex(['update_type', 'update_type_id'], 'c_ututi'); } if (!$schema->hasTable('circles_debug')) { diff --git a/lib/Model/Federated/EventWrapper.php b/lib/Model/Federated/EventWrapper.php index 7338941df..267aac678 100644 --- a/lib/Model/Federated/EventWrapper.php +++ b/lib/Model/Federated/EventWrapper.php @@ -31,11 +31,12 @@ namespace OCA\Circles\Model\Federated; +use JsonSerializable; use OCA\Circles\Tools\Db\IQueryRow; use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Model\ReferencedDataStore; use OCA\Circles\Tools\Model\SimpleDataStore; use OCA\Circles\Tools\Traits\TArrayTools; -use JsonSerializable; /** * Class EventWrapper @@ -51,6 +52,9 @@ class EventWrapper implements IQueryRow, JsonSerializable { public const STATUS_DONE = 8; public const STATUS_OVER = 9; + public const TYPE_BROADCAST = 'broadcast'; + public const TYPE_INTERNAL = 'internal'; + /** @var string */ private $token = ''; @@ -58,6 +62,9 @@ class EventWrapper implements IQueryRow, JsonSerializable { /** @var FederatedEvent */ private $event; + private string $eventType; + private ?ReferencedDataStore $store = null; + /** @var SimpleDataStore */ private $result; @@ -80,7 +87,8 @@ class EventWrapper implements IQueryRow, JsonSerializable { private $creation; - public function __construct() { + public function __construct(string $eventType = '') { + $this->eventType = $eventType; $this->result = new SimpleDataStore(); } @@ -130,6 +138,51 @@ public function hasEvent(): bool { } + /** + * @return ReferencedDataStore + */ + public function getStore(): ReferencedDataStore { + return $this->store; + } + + /** + * @param ReferencedDataStore $store + * + * @return self + */ + public function setStore(ReferencedDataStore $store): self { + $this->store = $store; + + return $this; + } + + /** + * @return bool + */ + public function hasStore(): bool { + return ($this->store !== null); + } + + + /** + * @param string $eventType + * + * @return EventWrapper + */ + public function setEventType(string $eventType): self { + $this->eventType = $eventType; + + return $this; + } + + /** + * @return string + */ + public function getEventType(): string { + return $this->eventType; + } + + /** * @param SimpleDataStore $result * @@ -262,6 +315,45 @@ public function setCreation(int $creation): self { } + /** + * @param array $data + * + * @return IQueryRow + * @throws InvalidItemException + */ + public function importFromDatabase(array $data): IQueryRow { + $this->setToken($this->get('token', $data)); + $this->setInstance($this->get('instance', $data)); + $this->setEventType($this->get('event_type', $data)); + $this->setInterface($this->getInt('interface', $data)); + $this->setSeverity($this->getInt('severity', $data, FederatedEvent::SEVERITY_LOW)); + $this->setStatus($this->getInt('status', $data, self::STATUS_INIT)); + + if ($this->getEventType() === self::TYPE_BROADCAST) { + $event = new FederatedEvent(); + $event->import($this->getArray('event', $data)); + $this->setEvent($event); + } + + if ($this->getEventType() === self::TYPE_INTERNAL) { + $store = new ReferencedDataStore(); + $store->import($this->getArray('store', $data)); + $this->setStore($store); + } + +// try { +// $store = new ReferencedDataStore(); +// $store->import($this->getArray('store', $data)); +// $this->setStore($store); +// } catch (InvalidItemException $e) { +// } + + $this->setResult(new SimpleDataStore($this->getArray('result', $data))); + + return $this; + } + + /** * @param array $data * @@ -271,13 +363,22 @@ public function setCreation(int $creation): self { public function import(array $data): self { $this->setToken($this->get('token', $data)); $this->setInstance($this->get('instance', $data)); + $this->setEventType($this->get('eventType', $data)); $this->setInterface($this->getInt('interface', $data)); $this->setSeverity($this->getInt('severity', $data, FederatedEvent::SEVERITY_LOW)); $this->setStatus($this->getInt('status', $data, self::STATUS_INIT)); - $event = new FederatedEvent(); - $event->import($this->getArray('event', $data)); - $this->setEvent($event); + if ($this->getEventType() === self::TYPE_BROADCAST) { + $event = new FederatedEvent(); + $event->import($this->getArray('event', $data)); + $this->setEvent($event); + } + + if ($this->getEventType() === self::TYPE_INTERNAL) { + $store = new ReferencedDataStore(); + $store->import($this->getArray('store', $data)); + $this->setStore($store); + } $this->setResult(new SimpleDataStore($this->getArray('result', $data))); $this->setCreation($this->getInt('creation', $data)); @@ -293,35 +394,14 @@ public function jsonSerialize(): array { return [ 'token' => $this->getToken(), 'instance' => $this->getInstance(), + 'eventType' => $this->getEventType(), 'interface' => $this->getInterface(), - 'event' => $this->getEvent(), + 'event' => ($this->hasEvent()) ? $this->getEvent() : null, + 'store' => ($this->hasStore()) ? $this->getStore() : null, 'result' => $this->getResult(), 'severity' => $this->getSeverity(), 'status' => $this->getStatus() // 'creation' => $this->getCreation() ]; } - - - /** - * @param array $data - * - * @return IQueryRow - * @throws InvalidItemException - */ - public function importFromDatabase(array $data): IQueryRow { - $this->setToken($this->get('token', $data)); - $this->setInstance($this->get('instance', $data)); - $this->setInterface($this->getInt('interface', $data)); - $this->setSeverity($this->getInt('severity', $data, FederatedEvent::SEVERITY_LOW)); - $this->setStatus($this->getInt('status', $data, self::STATUS_INIT)); - - $event = new FederatedEvent(); - $event->import($this->getArray('event', $data)); - $this->setEvent($event); - - $this->setResult(new SimpleDataStore($this->getArray('result', $data))); - - return $this; - } } diff --git a/lib/Model/FederatedUser.php b/lib/Model/FederatedUser.php index ac6a7ca2e..bfcf25e29 100644 --- a/lib/Model/FederatedUser.php +++ b/lib/Model/FederatedUser.php @@ -31,19 +31,19 @@ namespace OCA\Circles\Model; -use OCA\Circles\Tools\Db\IQueryRow; -use OCA\Circles\Tools\Exceptions\InvalidItemException; -use OCA\Circles\Tools\IDeserializable; -use OCA\Circles\Tools\Traits\TDeserialize; -use OCA\Circles\Tools\Traits\TArrayTools; use JsonSerializable; use OCA\Circles\Exceptions\FederatedUserNotFoundException; use OCA\Circles\Exceptions\MembershipNotFoundException; use OCA\Circles\Exceptions\OwnerNotFoundException; use OCA\Circles\Exceptions\RequestBuilderException; use OCA\Circles\Exceptions\UnknownInterfaceException; -use OCA\Circles\IFederatedUser; use OCA\Circles\IEntity; +use OCA\Circles\IFederatedUser; +use OCA\Circles\Tools\Db\IQueryRow; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\IDeserializable; +use OCA\Circles\Tools\Traits\TArrayTools; +use OCA\Circles\Tools\Traits\TDeserialize; /** * Class FederatedUser diff --git a/lib/Model/SyncedItemLock.php b/lib/Model/SyncedItemLock.php index 47f8ff045..86585c5ff 100644 --- a/lib/Model/SyncedItemLock.php +++ b/lib/Model/SyncedItemLock.php @@ -42,7 +42,7 @@ class SyncedItemLock implements IDeserializable, IQueryRow, JsonSerializable { use TArrayTools; private int $id; - private string $singleId; +// private string $singleId; private string $updateType; private string $updateTypeId; private int $time; @@ -79,23 +79,23 @@ public function getId(): int { } - /** - * @param string $singleId - * - * @return SyncedItemLock - */ - public function setSingleId(string $singleId): self { - $this->singleId = $singleId; - - return $this; - } - - /** - * @return string - */ - public function getSingleId(): string { - return $this->singleId; - } +// /** +// * @param string $singleId +// * +// * @return SyncedItemLock +// */ +// public function setSingleId(string $singleId): self { +// $this->singleId = $singleId; +// +// return $this; +// } +// +// /** +// * @return string +// */ +// public function getSingleId(): string { +// return $this->singleId; +// } /** * @param string $updateType @@ -180,11 +180,11 @@ public function isVerifyChecksum(): bool { * @throws InvalidItemException */ public function import(array $data): IDeserializable { - if ($this->getInt('singleId', $data) === 0) { - throw new InvalidItemException(); - } +// if ($this->getInt('singleId', $data) === 0) { +// throw new InvalidItemException(); +// } - $this->setSingleId($this->get('singleId', $data)); +// $this->setSingleId($this->get('singleId', $data)); $this->setUpdateType($this->get('updateType', $data)); $this->setUpdateTypeId($this->get('updateTypeId', $data)); $this->setTime($this->getInt('time', $data)); @@ -205,7 +205,7 @@ public function importFromDatabase(array $data, string $prefix = ''): IQueryRow throw new ShareTokenNotFoundException(); } - $this->setSingleId($this->get($prefix . 'single_id', $data)); +// $this->setSingleId($this->get($prefix . 'single_id', $data)); $this->setUpdateType($this->get($prefix . 'update_type', $data)); $this->setUpdateTypeId($this->get($prefix . 'update_type_id', $data)); $this->setTime($this->getInt($prefix . 'time', $data)); @@ -218,7 +218,7 @@ public function importFromDatabase(array $data, string $prefix = ''): IQueryRow */ public function jsonSerialize(): array { return [ - 'singleId' => $this->getSingleId(), +// 'singleId' => $this->getSingleId(), 'updateType' => $this->getUpdateType(), 'updateTypeId' => $this->getUpdateTypeId(), 'time' => $this->getTime(), diff --git a/lib/Model/SyncedWrapper.php b/lib/Model/SyncedWrapper.php index e4d4081a5..576f02ed0 100644 --- a/lib/Model/SyncedWrapper.php +++ b/lib/Model/SyncedWrapper.php @@ -45,18 +45,21 @@ class SyncedWrapper implements IReferencedObject, JsonSerializable { private ?IFederatedUser $federatedUser; private ?SyncedItem $item; + private ?SyncedItemLock $lock; private ?SyncedShare $share; private array $extraData; public function __construct( ?IFederatedUser $federatedUser = null, ?SyncedItem $item = null, + ?SyncedItemLock $lock = null, ?SyncedShare $share = null, array $extraData = [] ) { $this->federatedUser = $federatedUser; $this->item = $item; $this->share = $share; + $this->lock = $lock; $this->extraData = $extraData; } @@ -113,6 +116,32 @@ public function getItem(): ?SyncedItem { } + /** + * @param SyncedItem $lock + * + * @return SyncedWrapper + */ + public function setLock(SyncedItemLock $lock): self { + $this->lock = $lock; + + return $this; + } + + /** + * @return bool + */ + public function hasLock(): bool { + return !is_null($this->lock); + } + + /** + * @return SyncedItemLock + */ + public function getLock(): ?SyncedItemLock { + return $this->lock; + } + + /** * @param SyncedShare $share * diff --git a/lib/Service/AsyncService.php b/lib/Service/AsyncService.php new file mode 100644 index 000000000..7bd6e0fee --- /dev/null +++ b/lib/Service/AsyncService.php @@ -0,0 +1,254 @@ + + * @copyright 2022 + * @license GNU AGPL version 3 or any later version + * + * This program 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. + * + * This program 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 Affero General Public License + * along with this program. If not, see . + * + */ + + +namespace OCA\Circles\Service; + +use OC; +use OCA\Circles\Db\EventWrapperRequest; +use OCA\Circles\Exceptions\InternalAsyncException; +use OCA\Circles\IInternalAsync; +use OCA\Circles\Model\Circle; +use OCA\Circles\Model\Federated\EventWrapper; +use OCA\Circles\Model\Federated\FederatedEvent; +use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Exceptions\RequestNetworkException; +use OCA\Circles\Tools\Model\NCRequest; +use OCA\Circles\Tools\Model\ReferencedDataStore; +use OCA\Circles\Tools\Model\Request; +use OCA\Circles\Tools\Traits\TAsync; +use OCA\Circles\Tools\Traits\TNCRequest; +use OCA\Circles\Tools\Traits\TStringTools; +use ReflectionClass; +use ReflectionException; + +class AsyncService { + use TAsync; + use TStringTools; + use TNCRequest; + + private EventWrapperRequest $eventWrapperRequest; + private ConfigService $configService; + + private bool $asynced = false; + private bool $splittable = false; + + public function __construct( + EventWrapperRequest $eventWrapperRequest, + ConfigService $configService + ) { + $this->eventWrapperRequest = $eventWrapperRequest; + $this->configService = $configService; + } + + + /** + * split the process, anything after calling this method is out of main process. + * Will only work if isSplittable() is true. + */ + public function split(string $reason = ''): void { + if (!$this->isSplittable()) { + return; + } + + $this->async($reason); + $this->setAsynced(); + } + + public function isSplittable(): bool { + return $this->splittable; + } + + public function setSplittable(bool $splittable): void { + $this->splittable = $splittable; + } + + /** + * call this method only from WrappedEventController to confirm further process that + * we are already not running on a main process. + * + * @param bool $asynced + */ + public function setAsynced(bool $asynced = true): void { + $this->asynced = $asynced; + } + + public function isAsynced(): bool { + return $this->asynced; + } + + + /** + * @throws RequestNetworkException + */ + public function asyncBroadcast(FederatedEvent $event, array $instances) { + if (empty($instances) && !$event->isAsync()) { + return; + } + + $wrapper = new EventWrapper(EventWrapper::TYPE_BROADCAST); + $wrapper->setEvent($event); + $wrapper->setToken($this->uuid()); + $wrapper->setCreation(time()); + $wrapper->setSeverity($event->getSeverity()); + + if ($event->isAsync()) { + $wrapper->setInstance($this->configService->getLoopbackInstance()); + $this->eventWrapperRequest->save($wrapper); + } + + foreach ($instances as $instance) { + if ($event->getCircle()->isConfig(Circle::CFG_LOCAL)) { + break; + } + + $wrapper->setInstance($instance->getInstance()); + $wrapper->setInterface($instance->getInterface()); + // TODO: implement single save of multiple wrappers to avoid 10+ queries on big circles with + // a lot of instances + $this->eventWrapperRequest->save($wrapper); + } + + $event->setWrapperToken($wrapper->getToken()); + + if ($this->isAsynced()) { + // we're not on main process, we run the broadcast on this thread. + // Also cannot add EventWrapperService to DI or loop. + /** @var EventWrapperService $eventWrapperService */ + $eventWrapperService = \OC::$server->get(EventWrapperService::class); + $eventWrapperService->performBroadcast($wrapper->getToken()); + + return; + } + + try { + $request = new NCRequest('', Request::TYPE_POST); + $this->configService->configureLoopbackRequest( + $request, + 'circles.EventWrapper.asyncBroadcast', + ['token' => $wrapper->getToken()] + ); + + $this->doRequest($request); + } catch (RequestNetworkException $e) { + $this->e($e, ['wrapper' => $wrapper]); + } + } + + + /** + * @param string $internalAsync + * @param ReferencedDataStore|null $store + */ + public function asyncInternal(string $internalAsync, ?ReferencedDataStore $store = null): void { + \OC::$server->getLogger()->log(3, '###ASYNCED### ' . json_encode($this->isAsynced())); + + if (is_null($store)) { + $store = new ReferencedDataStore(); + } + + $store->s(IInternalAsync::STORE_INTERNAL_ASYNC, $internalAsync); + + $wrapper = new EventWrapper(EventWrapper::TYPE_INTERNAL); + $wrapper->setStore($store); + $wrapper->setToken($this->uuid()); + $wrapper->setCreation(time()); + + $this->eventWrapperRequest->save($wrapper); + + if ($this->isAsynced()) { + // we're not on main process, we run it on this thread. + // Also cannot add EventWrapperService to DI or loop. + /** @var EventWrapperService $eventWrapperService */ + $eventWrapperService = \OC::$server->get(EventWrapperService::class); + $eventWrapperService->performInternal($wrapper->getToken()); + + return; + } + + $request = new NCRequest('', Request::TYPE_POST); + $this->configService->configureLoopbackRequest( + $request, + 'circles.EventWrapper.asyncInternal', + ['token' => $wrapper->getToken()] + ); + + try { + $this->doRequest($request); + } catch (RequestNetworkException $e) { + $this->e($e, ['wrapper' => $wrapper]); + } + } + + + /** + * @param EventWrapper $wrapper + * + * @throws InternalAsyncException + * @throws InvalidItemException + */ + public function runInternalAsync(EventWrapper $wrapper): void { + $store = $wrapper->getStore(); + $internalAsync = $this->getInternalAsync($store); + + $store->u(IInternalAsync::STORE_INTERNAL_ASYNC); + $internalAsync->runAsynced($store); + } + + + /** + * @param ReferencedDataStore $store + * + * @return IInternalAsync + * @throws InvalidItemException + * @throws InternalAsyncException + */ + private function getInternalAsync(ReferencedDataStore $store): IInternalAsync { + $class = $store->g(IInternalAsync::STORE_INTERNAL_ASYNC); + + try { + $test = new ReflectionClass($class); + } catch (ReflectionException $e) { + throw new InternalAsyncException('ReflectionException with ' . $class . ': ' . $e->getMessage()); + } + + if (!in_array(IInternalAsync::class, $test->getInterfaceNames())) { + throw new InternalAsyncException($class . ' does not implements IInternalAsync'); + } + + $item = OC::$server->get($class); + if (!($item instanceof IInternalAsync)) { + throw new InternalAsyncException($class . ' not an IInternalAsync'); + } + + return $item; + } + +} diff --git a/lib/Service/EventWrapperService.php b/lib/Service/EventWrapperService.php index 07ad792d1..79e6ec4d4 100644 --- a/lib/Service/EventWrapperService.php +++ b/lib/Service/EventWrapperService.php @@ -32,6 +32,7 @@ use Exception; use OCA\Circles\Db\EventWrapperRequest; +use OCA\Circles\Exceptions\EventWrapperNotFoundException; use OCA\Circles\Model\Federated\EventWrapper; use OCA\Circles\Model\Federated\FederatedEvent; use OCA\Circles\Tools\ActivityPub\NCSignature; @@ -68,6 +69,8 @@ class EventWrapperService extends NCSignature { /** @var RemoteUpstreamService */ private $remoteUpstreamService; + private AsyncService $asyncService; + /** @var ConfigService */ private $configService; @@ -78,17 +81,20 @@ class EventWrapperService extends NCSignature { * @param EventWrapperRequest $eventWrapperRequest * @param FederatedEventService $federatedEventService * @param RemoteUpstreamService $remoteUpstreamService + * @param AsyncService $asyncService * @param ConfigService $configService */ public function __construct( EventWrapperRequest $eventWrapperRequest, FederatedEventService $federatedEventService, RemoteUpstreamService $remoteUpstreamService, + AsyncService $asyncService, ConfigService $configService ) { $this->eventWrapperRequest = $eventWrapperRequest; $this->federatedEventService = $federatedEventService; $this->remoteUpstreamService = $remoteUpstreamService; + $this->asyncService = $asyncService; $this->configService = $configService; } @@ -98,7 +104,7 @@ public function __construct( * @param bool $refresh */ public function confirmStatus(string $token, bool $refresh = false): void { - $wrappers = $this->eventWrapperRequest->getByToken($token); + $wrappers = $this->eventWrapperRequest->getBroadcastByToken($token); foreach ($wrappers as $wrapper) { $status = $wrapper->getStatus(); @@ -181,4 +187,56 @@ function (EventWrapper $event): string { return array_values(array_unique($token)); } + + + /** + * @param string $token + * + * @return EventWrapper[] + */ + public function getBroadcastByToken(string $token): array { + return $this->eventWrapperRequest->getBroadcastByToken($token); + } + + /** + * @param string $token + * + * @return EventWrapper + * @throws EventWrapperNotFoundException + */ + public function getInternalEventByToken(string $token): EventWrapper { + return $this->eventWrapperRequest->getInternalByToken($token); + } + + + /** + * @param string $token + */ + public function performInternal(string $token): void { + try { + $wrapper = $this->getInternalEventByToken($token); + $this->asyncService->runInternalAsync($wrapper); + } catch (EventWrapperNotFoundException $e) { + } + + // TODO: delete token in table. + } + + + /** + * @param string $token + * @param array|null $wrappers + */ + public function performBroadcast(string $token, ?array $wrappers = null): void { + if (is_null($wrappers)) { + $wrappers = $this->getBroadcastByToken($token); + } + + foreach ($wrappers as $wrapper) { + $this->manageWrapper($wrapper); + } + + $this->confirmStatus($token); + } + } diff --git a/lib/Service/FederatedEventService.php b/lib/Service/FederatedEventService.php index a715a6a6e..1c7b762a3 100644 --- a/lib/Service/FederatedEventService.php +++ b/lib/Service/FederatedEventService.php @@ -65,8 +65,6 @@ use OCA\Circles\Model\Member; use OCA\Circles\Tools\ActivityPub\NCSignature; use OCA\Circles\Tools\Exceptions\RequestNetworkException; -use OCA\Circles\Tools\Model\NCRequest; -use OCA\Circles\Tools\Model\Request; use OCA\Circles\Tools\Traits\TNCRequest; use OCA\Circles\Tools\Traits\TStringTools; use ReflectionClass; @@ -97,6 +95,8 @@ class FederatedEventService extends NCSignature { /** @var InterfaceService */ private $interfaceService; + private AsyncService $asyncService; + /** @var ConfigService */ private $configService; @@ -111,6 +111,7 @@ class FederatedEventService extends NCSignature { * @param MemberRequest $memberRequest * @param RemoteUpstreamService $remoteUpstreamService * @param InterfaceService $interfaceService + * @param AsyncService $asyncService * @param ConfigService $configService * @param DebugService $debugService */ @@ -120,6 +121,7 @@ public function __construct( MemberRequest $memberRequest, RemoteUpstreamService $remoteUpstreamService, InterfaceService $interfaceService, + AsyncService $asyncService, ConfigService $configService, DebugService $debugService ) { @@ -128,6 +130,7 @@ public function __construct( $this->memberRequest = $memberRequest; $this->remoteUpstreamService = $remoteUpstreamService; $this->interfaceService = $interfaceService; + $this->asyncService = $asyncService; $this->configService = $configService; $this->debugService = $debugService; } @@ -388,46 +391,7 @@ private function configureEvent(FederatedEvent $event, IFederatedItem $item) { * @throws RequestBuilderException */ public function initBroadcast(FederatedEvent $event): void { - $instances = $this->getInstances($event); - if (empty($instances) && !$event->isAsync()) { - return; - } - - $wrapper = new EventWrapper(); - $wrapper->setEvent($event); - $wrapper->setToken($this->uuid()); - $wrapper->setCreation(time()); - $wrapper->setSeverity($event->getSeverity()); - - if ($event->isAsync()) { - $wrapper->setInstance($this->configService->getLoopbackInstance()); - $this->eventWrapperRequest->save($wrapper); - } - - foreach ($instances as $instance) { - if ($event->getCircle()->isConfig(Circle::CFG_LOCAL)) { - break; - } - - $wrapper->setInstance($instance->getInstance()); - $wrapper->setInterface($instance->getInterface()); - $this->eventWrapperRequest->save($wrapper); - } - - $request = new NCRequest('', Request::TYPE_POST); - $this->configService->configureLoopbackRequest( - $request, - 'circles.EventWrapper.asyncBroadcast', - ['token' => $wrapper->getToken()] - ); - - $event->setWrapperToken($wrapper->getToken()); - - try { - $this->doRequest($request); - } catch (RequestNetworkException $e) { - $this->e($e, ['wrapper' => $wrapper]); - } + $this->asyncService->asyncBroadcast($event, $this->getInstances($event)); } @@ -488,7 +452,7 @@ function (RemoteInstance $instance): string { * @param string $token */ public function manageResults(string $token): void { - $wrappers = $this->eventWrapperRequest->getByToken($token); + $wrappers = $this->eventWrapperRequest->getBroadcastByToken($token); $event = null; $results = []; diff --git a/lib/Service/FederatedSyncItemService.php b/lib/Service/FederatedSyncItemService.php index f10f52d37..b40c1b702 100644 --- a/lib/Service/FederatedSyncItemService.php +++ b/lib/Service/FederatedSyncItemService.php @@ -33,12 +33,14 @@ use Exception; use OCA\Circles\Db\CircleRequest; use OCA\Circles\Db\RemoteRequest; +use OCA\Circles\Db\SyncedItemLockRequest; use OCA\Circles\Db\SyncedItemRequest; use OCA\Circles\Db\SyncedShareRequest; use OCA\Circles\Exceptions\FederatedEventException; use OCA\Circles\Exceptions\FederatedItemException; use OCA\Circles\Exceptions\FederatedSyncConflictException; use OCA\Circles\Exceptions\FederatedSyncManagerNotFoundException; +use OCA\Circles\Exceptions\FederatedSyncPermissionException; use OCA\Circles\Exceptions\InitiatorNotConfirmedException; use OCA\Circles\Exceptions\InvalidIdException; use OCA\Circles\Exceptions\OwnerNotFoundException; @@ -46,67 +48,71 @@ use OCA\Circles\Exceptions\RemoteNotFoundException; use OCA\Circles\Exceptions\RemoteResourceNotFoundException; use OCA\Circles\Exceptions\RequestBuilderException; +use OCA\Circles\Exceptions\SyncedItemLockException; use OCA\Circles\Exceptions\SyncedItemNotFoundException; use OCA\Circles\Exceptions\UnknownRemoteException; use OCA\Circles\FederatedItems\FederatedSync\ItemUpdate; use OCA\Circles\IFederatedUser; +use OCA\Circles\InternalAsync\AsyncItemUpdate; use OCA\Circles\Model\Circle; use OCA\Circles\Model\Federated\FederatedEvent; use OCA\Circles\Model\Federated\RemoteInstance; use OCA\Circles\Model\SyncedItem; +use OCA\Circles\Model\SyncedItemLock; use OCA\Circles\Model\SyncedShare; use OCA\Circles\Model\SyncedWrapper; use OCA\Circles\Tools\ActivityPub\NCSignature; use OCA\Circles\Tools\Exceptions\InvalidItemException; +use OCA\Circles\Tools\Model\ReferencedDataStore; use OCA\Circles\Tools\Model\Request; -use OCA\Circles\Tools\Model\SimpleDataStore; +use OCA\Circles\Tools\Traits\TAsync; use OCA\Circles\Tools\Traits\TDeserialize; use OCA\Circles\Tools\Traits\TStringTools; class FederatedSyncItemService extends NCSignature { use TStringTools; use TDeserialize; + use TAsync; + + const LOCK_RETRY_LIMIT = 3; + const LOCK_TIMEOUT = 15; // in seconds private SyncedItemRequest $syncedItemRequest; private SyncedShareRequest $syncedShareRequest; + private SyncedItemLockRequest $syncedItemLockRequest; private CircleRequest $circleRequest; private RemoteRequest $remoteRequest; private FederatedSyncService $federatedSyncService; private FederatedEventService $federatedEventService; private RemoteStreamService $remoteStreamService; private InterfaceService $interfaceService; + private AsyncService $asyncService; private DebugService $debugService; - /** - * @param SyncedItemRequest $syncedItemRequest - * @param SyncedShareRequest $syncedShareRequest - * @param RemoteRequest $remoteRequest - * @param FederatedSyncService $federatedSyncService - * @param FederatedEventService $federatedEventService - * @param RemoteStreamService $remoteStreamService - * @param InterfaceService $interfaceService - * @param DebugService $debugService - */ public function __construct( SyncedItemRequest $syncedItemRequest, SyncedShareRequest $syncedShareRequest, + SyncedItemLockRequest $syncedItemLockRequest, CircleRequest $circleRequest, RemoteRequest $remoteRequest, FederatedSyncService $federatedSyncService, FederatedEventService $federatedEventService, RemoteStreamService $remoteStreamService, InterfaceService $interfaceService, + AsyncService $asyncService, DebugService $debugService ) { $this->syncedItemRequest = $syncedItemRequest; $this->syncedShareRequest = $syncedShareRequest; + $this->syncedItemLockRequest = $syncedItemLockRequest; $this->circleRequest = $circleRequest; $this->remoteRequest = $remoteRequest; $this->federatedSyncService = $federatedSyncService; $this->federatedEventService = $federatedEventService; $this->remoteStreamService = $remoteStreamService; $this->interfaceService = $interfaceService; + $this->asyncService = $asyncService; $this->debugService = $debugService; } @@ -270,25 +276,60 @@ public function initSyncedItem( } + /** + * @param IFederatedUser $federatedUser + * @param SyncedItem $syncedItem + * @param SyncedItemLock $syncedLock + * @param array $extraData + * @param bool $initiatedRemotely + * + * @return array + * @throws FederatedEventException + * @throws FederatedItemException + * @throws FederatedSyncConflictException + * @throws FederatedSyncManagerNotFoundException + * @throws InitiatorNotConfirmedException + * @throws OwnerNotFoundException + * @throws RemoteInstanceException + * @throws RemoteNotFoundException + * @throws RemoteResourceNotFoundException + * @throws RequestBuilderException + * @throws UnknownRemoteException + */ public function requestSyncedItemUpdate( IFederatedUser $federatedUser, SyncedItem $syncedItem, - array $extraData = [] - ): array { + SyncedItemLock $syncedLock, + array $extraData = [], + bool $initiatedRemotely = false + ): void { // confirm item is local if ($syncedItem->isLocal()) { - return $this->requestSyncedItemUpdateLocal($federatedUser, $syncedItem, $extraData); - } else { - return $this->requestSyncedItemUpdateRemote($federatedUser, $syncedItem, $extraData); + $this->requestSyncedItemUpdateLocal( + $federatedUser, + $syncedItem, + $syncedLock, + $extraData, + $initiatedRemotely + ); + + return; + } else if (!$initiatedRemotely) { + $this->requestSyncedItemUpdateRemote($federatedUser, $syncedItem, $syncedLock, $extraData); + + return; } + + throw new FederatedSyncConflictException(); } /** * @param IFederatedUser $federatedUser * @param SyncedItem $syncedItem + * @param SyncedItemLock $syncedLock * @param array $extraData * - * @return SyncedItem + * @return array * @throws FederatedEventException * @throws FederatedItemException * @throws FederatedSyncManagerNotFoundException @@ -299,29 +340,73 @@ public function requestSyncedItemUpdate( * @throws RemoteResourceNotFoundException * @throws RequestBuilderException * @throws UnknownRemoteException + * @throws FederatedSyncPermissionException */ private function requestSyncedItemUpdateLocal( IFederatedUser $federatedUser, SyncedItem $syncedItem, - array $extraData = [] - ): array { - $item = $this->isItemUpdatable($federatedUser, $syncedItem, $extraData); + SyncedItemLock $syncedLock, + array $extraData = [], + bool $initiatedRemotely = false + ): void { + // item will be lock during the process, only to be unlocked when new item checksum have + // been calculated (on async process) + // verify checksum and apps settings about the lock + $this->manageLock($syncedLock); + + if (!$this->isItemModifiable($federatedUser, $syncedItem, $syncedLock, $extraData)) { + throw new FederatedSyncPermissionException('item modification not allowed'); + } $syncManager = $this->federatedSyncService->initSyncManager($syncedItem); - $syncManager->syncItem( +// $syncManager->syncItem( +// $syncedItem->getItemId(), +// $item +// ); + + $syncManager->onItemModification( $syncedItem->getItemId(), - $item + $syncedLock->getUpdateType(), + $syncedLock->getUpdateTypeId(), + $extraData, + $federatedUser ); - $this->updateChecksum($syncedItem->getSingleId()); + // + // Move all code below out of this method ? + // + // +// if ($initiatedRemotely) { + $this->asyncService->split(); // we need to confirm this is good enough +// } + + $this->asyncService->asyncInternal( + AsyncItemUpdate::class, + new ReferencedDataStore( + [ + 'syncedItem' => $syncedItem, + 'syncedLock' => $syncedLock, + ] + ) + ); + + + // Async + // Checksum + // broadcast update + + +// $this->updateChecksum($syncedItem->getSingleId()); // broadcast update signal // TODO: if request origin is not local, Async here, return $item to the remote instance - $this->broadcastItemUpdate($syncedItem->getSingleId()); +// $this->broadcastItemUpdate($syncedItem->getSingleId()); // $syncedItem->setSerialized($item); - return $item; + $this->removeLock($syncedLock); + +// return $item; // // try { @@ -348,9 +433,10 @@ private function requestSyncedItemUpdateLocal( private function requestSyncedItemUpdateRemote( IFederatedUser $federatedUser, SyncedItem $syncedItem, + SyncedItemLock $syncedLock, array $extraData = [] ): array { - $wrapper = new SyncedWrapper($federatedUser, $syncedItem, null, $extraData); + $wrapper = new SyncedWrapper($federatedUser, $syncedItem, null, null, $extraData); $this->interfaceService->setCurrentInterfaceFromInstance($syncedItem->getInstance()); $data = $this->remoteStreamService->resultRequestRemoteInstance( $syncedItem->getInstance(), @@ -567,16 +653,18 @@ public function updateSyncedItem(SyncedItem $syncedItem): void { /** * @param IFederatedUser $federatedUser * @param SyncedItem $syncedItem + * @param SyncedItemLock $syncedLock * @param array $extraData * - * @return array + * @return bool * @throws FederatedSyncManagerNotFoundException */ - private function isItemUpdatable( + private function isItemModifiable( IFederatedUser $federatedUser, SyncedItem $syncedItem, + SyncedItemLock $syncedLock, array $extraData = [] - ): array { + ): bool { $syncManager = $this->federatedSyncService->initSyncManager($syncedItem); $this->debugService->info( 'sharing of SyncedItem {syncedItem.singleId} looks doable, calling {`isShareCreatable()} on {syncManager.class} for confirmation', @@ -588,29 +676,13 @@ private function isItemUpdatable( ] ); - try { - return $syncManager->isItemUpdatable( - $syncedItem->getItemId(), - $extraData, - $federatedUser - ); - } catch (Exception $e) { - $this->debugService->exception($e); -// $this->debugService->info( -// 'update of SyncedItem {!syncedItem.singleId} is blocked by {!syncedItem.appId}', -// '', -// [ -// 'federatedUser' => $federatedUser, -// 'syncedItem' => $syncedItem, -// 'extraData' => $extraData -// ] -// ); - // define Exception that can be thrown by app: - // - itemnotfound - // - ItemUpdateException - // - throw $e; - } + return $syncManager->isItemModifiable( + $syncedItem->getItemId(), + $syncedLock->getUpdateType(), + $syncedLock->getUpdateTypeId(), + $extraData, + $federatedUser + ); } @@ -708,4 +780,38 @@ private function compareWithKnownItemId(SyncedItem $syncedItem): void { } } + + /** + * @param SyncedItemLock $syncedLock + * + * @throws InvalidItemException + * @throws SyncedItemLockException + */ + private function manageLock(SyncedItemLock $syncedLock): void { + $locked = true; + for ($i = 0; $i < self::LOCK_RETRY_LIMIT; $i++) { + try { + $this->syncedItemLockRequest->clean(self::LOCK_TIMEOUT); + $this->syncedItemLockRequest->getSyncedItemLock($syncedLock); + sleep(1); + } catch (SyncedItemNotFoundException $e) { + $locked = false; + break; + } + } + + if ($locked) { + throw new SyncedItemLockException('item is currently lock, try again later'); + } + + $this->syncedItemLockRequest->save($syncedLock); + } + + + /** + * @param SyncedItemLock $syncedLock + */ + private function removeLock(SyncedItemLock $syncedLock): void { + $this->syncedItemLockRequest->remove($syncedLock); + } } diff --git a/lib/Service/GSUpstreamService.php b/lib/Service/GSUpstreamService.php index 7684f558a..3e24f2f31 100644 --- a/lib/Service/GSUpstreamService.php +++ b/lib/Service/GSUpstreamService.php @@ -287,7 +287,7 @@ private function isLocalEvent(GSEvent $event): bool { * @throws ModelException */ public function getEventsByToken(string $token): array { - return $this->eventWrapperRequest->getByToken($token); + return $this->eventWrapperRequest->getBroadcastByToken($token); } @@ -298,7 +298,7 @@ public function getEventsByToken(string $token): array { */ public function manageResults(string $token): void { try { - $wrappers = $this->eventWrapperRequest->getByToken($token); + $wrappers = $this->eventWrapperRequest->getBroadcastByToken($token); } catch (JsonException | ModelException $e) { return; } diff --git a/lib/Service/RemoteUpstreamService.php b/lib/Service/RemoteUpstreamService.php index e5346e845..d9c4d6cd1 100644 --- a/lib/Service/RemoteUpstreamService.php +++ b/lib/Service/RemoteUpstreamService.php @@ -31,9 +31,6 @@ namespace OCA\Circles\Service; -use OCA\Circles\Tools\Model\Request; -use OCA\Circles\Tools\Model\SimpleDataStore; -use OCA\Circles\Tools\Traits\TNCRequest; use OCA\Circles\Db\EventWrapperRequest; use OCA\Circles\Exceptions\FederatedItemException; use OCA\Circles\Exceptions\OwnerNotFoundException; @@ -44,6 +41,9 @@ use OCA\Circles\Model\Federated\EventWrapper; use OCA\Circles\Model\Federated\FederatedEvent; use OCA\Circles\Model\Federated\RemoteInstance; +use OCA\Circles\Tools\Model\Request; +use OCA\Circles\Tools\Model\SimpleDataStore; +use OCA\Circles\Tools\Traits\TNCRequest; /** * Class RemoteUpstreamService @@ -88,16 +88,6 @@ public function __construct( } - /** - * @param string $token - * - * @return EventWrapper[] - */ - public function getEventsByToken(string $token): array { - return $this->eventWrapperRequest->getByToken($token); - } - - /** * @param EventWrapper $wrapper * diff --git a/lib/Tools/Model/ReferencedDataStore.php b/lib/Tools/Model/ReferencedDataStore.php index b74c1ce3e..aff75e99a 100644 --- a/lib/Tools/Model/ReferencedDataStore.php +++ b/lib/Tools/Model/ReferencedDataStore.php @@ -62,8 +62,13 @@ class ReferencedDataStore implements IDeserializable, JsonSerializable { private array $data = []; private string $lock = IReferencedObject::class; - public function __construct(array $data = []) { - $this->data = $data; + public function __construct(array $mixed = []) { + foreach ($mixed as $k => $v) { + try { + $this->sMixed($k, $v); + } catch (InvalidItemException $e) { + } + } } @@ -101,6 +106,7 @@ public function g(string $key): string { public function u(string $key): self { if ($this->hasKey($key)) { unset($this->data[$key]); + unset($this->ref[$key]); } return $this; @@ -272,7 +278,7 @@ public function gObj(string $key): ?IDeserializable { * @return ReferencedDataStore * @throws InvalidItemException */ - public function sMixed(string $k, mixed $obj): self { + public function sMixed(string $k, $obj): self { if ($obj instanceof JsonSerializable) { return $this->sObj($k, $obj); } diff --git a/lib/Tools/Traits/TAsync.php b/lib/Tools/Traits/TAsync.php index 1a0b5de97..575fd61e8 100644 --- a/lib/Tools/Traits/TAsync.php +++ b/lib/Tools/Traits/TAsync.php @@ -36,9 +36,7 @@ trait TAsync { use TNCSetup; - - /** @var string */ - public static $SETUP_TIME_LIMIT = 'async_time_limit'; + public static string $SETUP_TIME_LIMIT = 'async_time_limit'; /**