diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index f3bbcab9..b5994b91 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -15,7 +15,6 @@ use OCA\FullTextSearch\Capabilities; use OCA\FullTextSearch\ConfigLexicon; use OCA\FullTextSearch\Search\UnifiedSearchProvider; -use OCA\FullTextSearch\Service\ConfigService; use OCA\FullTextSearch\Service\IndexService; use OCA\FullTextSearch\Service\ProviderService; use OCA\FullTextSearch\Service\SearchService; @@ -32,28 +31,12 @@ use Psr\Container\ContainerInterface; use Throwable; -if (file_exists($autoLoad = __DIR__ . '/../../vendor/autoload.php')) { - include_once $autoLoad; -} - class Application extends App implements IBootstrap { - const APP_ID = 'fulltextsearch'; - const APP_NAME = 'FullTextSearch'; - - - /** - * Application constructor. - * - * @param array $params - */ + public const APP_ID = 'fulltextsearch'; public function __construct(array $params = []) { parent::__construct(self::APP_ID, $params); } - - /** - * @param IRegistrationContext $context - */ public function register(IRegistrationContext $context): void { $context->registerCapability(Capabilities::class); $context->registerSearchProvider(UnifiedSearchProvider::class); diff --git a/lib/ConfigLexicon.php b/lib/ConfigLexicon.php index 3e205e71..df348e06 100644 --- a/lib/ConfigLexicon.php +++ b/lib/ConfigLexicon.php @@ -26,6 +26,8 @@ class ConfigLexicon implements ILexicon { public const COLLECTION_INDEXING_LIST = 'collection_indexing_list'; public const COLLECTION_INTERNAL = 'collection_internal'; public const COLLECTION_LINKS = 'collection_links'; + public const LOCK_ID = 'lock_id'; + public const LOCK_PING = 'lock_ping'; public function getStrictness(): Strictness { return Strictness::NOTICE; @@ -40,6 +42,9 @@ public function getAppConfigs(): array { new Entry(key: self::COLLECTION_INDEXING_LIST, type: ValueType::INT, defaultRaw: 50, definition: 'size of chunks of async documents on collection queue request'), new Entry(key: self::COLLECTION_INTERNAL, type: ValueType::STRING, defaultRaw: 'local', definition: 'name of the local collection'), new Entry(key: self::COLLECTION_LINKS, type: ValueType::ARRAY, defaultRaw: [], definition: '(internal) data relative to collections'), + // IAppConfig::FLAG_INTERNAL) + new Entry(key: self::LOCK_ID, type: ValueType::STRING, defaultRaw: '', definition: 'internal lock id', lazy: true), + new Entry(key: self::LOCK_PING, type: ValueType::INT, defaultRaw: 0, definition: 'internal lock time', lazy: true), ]; } diff --git a/lib/Db/TickRequest.php b/lib/Db/TickRequest.php deleted file mode 100644 index 3563658c..00000000 --- a/lib/Db/TickRequest.php +++ /dev/null @@ -1,149 +0,0 @@ -getTickInsertSql(); - $qb->setValue('source', $qb->createNamedParameter($tick->getSource())) - ->setValue('data', $qb->createNamedParameter(json_encode($tick->getData()))) - ->setValue('action', $qb->createNamedParameter($tick->getAction())) - ->setValue('first_tick', $qb->createNamedParameter($tick->getFirstTick())) - ->setValue('tick', $qb->createNamedParameter($tick->getTick())) - ->setValue('status', $qb->createNamedParameter($tick->getStatus())); - - try { - $qb->executeStatement(); - } catch (\OCP\DB\Exception $e) { - if ($e->getReason() === \OCP\DB\Exception::REASON_CONNECTION_LOST) { - $this->reconnect($e); - return $this->create($tick); - } - throw $e; - } - - return $qb->getLastInsertId(); - } catch (Exception $e) { - throw $e; - } - } - - /** - * @param Tick $tick - * - * @return bool - */ - public function update(Tick $tick): bool { - try { - $this->getTickById($tick->getId()); - } catch (TickDoesNotExistException $e) { - return false; - } - - $qb = $this->getTickUpdateSql(); - $qb->set('data', $qb->createNamedParameter(json_encode($tick->getData()))) - ->set('tick', $qb->createNamedParameter($tick->getTick())) - ->set('action', $qb->createNamedParameter($tick->getAction())) - ->set('status', $qb->createNamedParameter($tick->getStatus())); - - $this->limitToId($qb, $tick->getId()); - - try { - $qb->executeStatement(); - } catch (\OCP\DB\Exception $e) { - if ($e->getReason() === \OCP\DB\Exception::REASON_CONNECTION_LOST) { - $this->reconnect($e); - return $this->update($tick); - } - throw $e; - } - - return true; - } - - /** - * return tick. - * - * @param int $id - * - * @return Tick - * @throws TickDoesNotExistException - */ - public function getTickById(int $id): Tick { - $qb = $this->getTickSelectSql(); - $this->limitToId($qb, $id); - - try { - $cursor = $qb->executeQuery(); - } catch (\OCP\DB\Exception $e) { - if ($e->getReason() === \OCP\DB\Exception::REASON_CONNECTION_LOST) { - $this->reconnect($e); - return $this->getTickById($id); - } - throw $e; - } - - $data = $cursor->fetch(); - $cursor->closeCursor(); - - if ($data === false) { - throw new TickDoesNotExistException($this->l10n->t('Process timed out')); - } - - return $this->parseTickSelectSql($data); - } - - /** - * return ticks. - * - * @param string $status - * - * @return Tick[] - */ - public function getTicksByStatus(string $status): array { - $ticks = []; - - $qb = $this->getTickSelectSql(); - $this->limitToStatus($qb, $status); - - try { - $cursor = $qb->executeQuery(); - } catch (\OCP\DB\Exception $e) { - if ($e->getReason() === \OCP\DB\Exception::REASON_CONNECTION_LOST) { - $this->reconnect($e); - return $this->getTicksByStatus($status); - } - throw $e; - } - - while ($data = $cursor->fetch()) { - $ticks[] = $this->parseTickSelectSql($data); - } - $cursor->closeCursor(); - - return $ticks; - } -} diff --git a/lib/Db/TickRequestBuilder.php b/lib/Db/TickRequestBuilder.php deleted file mode 100644 index f5a85bc6..00000000 --- a/lib/Db/TickRequestBuilder.php +++ /dev/null @@ -1,103 +0,0 @@ -dbConnection->getQueryBuilder(); - $qb->insert(self::TABLE_TICKS); - - return $qb; - } - - - /** - * Base of the Sql Update request - * - * @return IQueryBuilder - */ - protected function getTickUpdateSql(): IQueryBuilder { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->update(self::TABLE_TICKS); - - return $qb; - } - - - /** - * Base of the Sql Select request for Shares - * - * @return IQueryBuilder - */ - protected function getTickSelectSql(): IQueryBuilder { - $qb = $this->dbConnection->getQueryBuilder(); - - /** @noinspection PhpMethodParametersCountMismatchInspection */ - $qb->select( - 't.id', 't.source', 't.data', 't.first_tick', 't.tick', 't.status', 't.action' - ) - ->from(self::TABLE_TICKS, 't'); - - $this->defaultSelectAlias = 't'; - - return $qb; - } - - - /** - * Base of the Sql Delete request - * - * @return IQueryBuilder - */ - protected function getTickDeleteSql(): IQueryBuilder { - $qb = $this->dbConnection->getQueryBuilder(); - $qb->delete(self::TABLE_TICKS); - - return $qb; - } - - - /** - * @param array $data - * - * @return Tick - */ - protected function parseTickSelectSql(array $data): Tick { - $tick = new Tick($this->get('source', $data, ''), $this->getInt('id', $data, 0)); - $tick->setData($this->getArray('data', $data, [])) - ->setTick($this->getInt('tick', $data, 0)) - ->setFirstTick($this->getInt('first_tick', $data, 0)) - ->setStatus($this->get('status', $data, '')) - ->setAction($this->get('action', $data, '')); - - return $tick; - } - -} diff --git a/lib/Exceptions/LockException.php b/lib/Exceptions/LockException.php new file mode 100644 index 00000000..3a965fbb --- /dev/null +++ b/lib/Exceptions/LockException.php @@ -0,0 +1,14 @@ +hasTable('fulltextsearch_ticks')) { + return null; + } + + $schema->dropTable('fulltextsearch_ticks'); + return $schema; + } +} diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index d23fd40d..7dcfa7c9 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -16,7 +16,6 @@ class ConfigService { public function __construct( private readonly IAppConfig $appConfig, - private readonly IConfig $config, ) { } @@ -57,12 +56,6 @@ public function setConfig(array $save): void { } } - public function getAppValue(string $key): string { - return $this->config->getSystemValueString(Application::APP_ID . '.' . $key, - (string)$this->appConfig->getAppValueString($key) - ); - } - public function getInternalCollection(): string { return $this->appConfig->getAppValueString(ConfigLexicon::COLLECTION_INTERNAL); } diff --git a/lib/Service/LockService.php b/lib/Service/LockService.php new file mode 100644 index 00000000..cfb5656a --- /dev/null +++ b/lib/Service/LockService.php @@ -0,0 +1,99 @@ +lockId = $random->generate(7); + } + + /** + * Lock the index to this process for few minutes. + * Needs to be refreshed to keep lock alive. + * + * _**Warning:** reload lazy app config._ + * + * @throws LockException if the index is already running + */ + public function lock(): void { + $this->appConfig->clearCache(true); + $currentLockPing = $this->appConfig->getValueInt(Application::APP_ID, ConfigLexicon::LOCK_PING); + $currentLockId = $this->appConfig->getValueString(Application::APP_ID, ConfigLexicon::LOCK_ID); + + // previous lock timeout + if ($currentLockPing < time()) { + $currentLockId = ''; + } + + // new lock; enforce ping on new lock + if ($currentLockId === '') { + $this->appConfig->setValueString(Application::APP_ID, ConfigLexicon::LOCK_ID, $this->lockId); + $currentLockId = $this->lockId; + $this->nextPing = 0; + } + + // confirm the lock belongs to the current process + if ($currentLockId !== $this->lockId) { + throw new LockException('Index is already running'); + } + + $this->update(); + } + + public function update(): void { + if ($this->nextPing === -1) { + throw new LockException('Lock service not initiated on this process'); + } + + $time = time(); + + // do not flood database + if ($this->nextPing > $time) { + return; + } + + $this->appConfig->clearCache(true); + $currentLockId = $this->appConfig->getValueString(Application::APP_ID, ConfigLexicon::LOCK_ID); + + // new lock; enforce ping on new lock + if ($currentLockId === '') { + throw new LockException('Index not locked'); + } + + // confirm the lock belongs to the current process + if ($currentLockId !== $this->lockId) { + throw new LockException('Index locked by another process'); + } + + // update ping + $this->nextPing = $time + self::LOCK_PING_DELAY; + $this->appConfig->setValueInt(Application::APP_ID, ConfigLexicon::LOCK_PING, $this->nextPing + self::LOCK_TIMEOUT); + } + + public function unlock(): void { + $this->appConfig->deleteKey(Application::APP_ID, ConfigLexicon::LOCK_PING); + $this->nextPing = -1; + } +} diff --git a/lib/Service/RunningService.php b/lib/Service/RunningService.php index a90c8530..8337d657 100644 --- a/lib/Service/RunningService.php +++ b/lib/Service/RunningService.php @@ -9,182 +9,46 @@ namespace OCA\FullTextSearch\Service; -use Exception; -use OCA\FullTextSearch\ConfigLexicon; -use OCA\FullTextSearch\Db\TickRequest; +use OCA\FullTextSearch\Exceptions\LockException; use OCA\FullTextSearch\Exceptions\RunnerAlreadyUpException; -use OCA\FullTextSearch\Exceptions\TickDoesNotExistException; -use OCA\FullTextSearch\Exceptions\TickIsNotAliveException; -use OCA\FullTextSearch\Model\Tick; -use OCP\AppFramework\Services\IAppConfig; +/** + * @deprecated + * @see LockService + */ class RunningService { public function __construct( - private TickRequest $tickRequest, - private readonly IAppConfig $appConfig, + private readonly LockService $lockService, ) { } /** - * @param string $source - * - * @return int - * @throws RunnerAlreadyUpException - * @throws Exception + * @deprecated */ public function start(string $source): int { - - if ($this->isAlreadyRunning()) { - throw new RunnerAlreadyUpException('Index is already running'); + try { + $this->lockService->lock(); + } catch (LockException $e) { + throw new RunnerAlreadyUpException($e->getMessage()); } - - $tick = new Tick($source); - $tick->setStatus('run') - ->setTick() - ->setFirstTick() - ->setInfoInt('runStart ', time()); - - return $this->tickRequest->create($tick); + return 1; } - /** - * @param int $runId - * @param string $action - * - * @throws TickDoesNotExistException - * @throws TickIsNotAliveException + * @deprecated */ public function update(int $runId, string $action = '') { - $tick = $this->tickRequest->getTickById($runId); - - $this->isStillAlive($tick, true); - $tick->setTick(); - - if ($action !== '' && $action !== $tick->getAction()) { - $this->assignActionToTick($tick, $action); - } - - $this->tickRequest->update($tick); - } - - - /** - * @param int $runId - * @param string $reason - * - * @throws TickDoesNotExistException - */ - public function stop(int $runId, string $reason = '') { - $tick = $this->tickRequest->getTickById($runId); - $tick->setStatus('stop') - ->setTick() - ->setInfoInt('runStop', time()) - ->setInfoInt('totalDocuments', 42); - - if ($reason !== '') { - $tick->setStatus('exception'); - $tick->setInfo('exception', $reason); - } - - $this->tickRequest->update($tick); - } - - - /** - * @param int $runId - * - * @return bool - * @throws TickIsNotAliveException - */ - public function isAlive(int $runId): bool { - $tick = null; try { - $tick = $this->tickRequest->getTickById($runId); - } catch (TickDoesNotExistException $e) { - return false; - } - - return $this->isStillAlive($tick); - } - - - /** - * @param Tick $tick - * @param bool $exception - * - * @return bool - * @throws TickIsNotAliveException - */ - public function isStillAlive(Tick $tick, bool $exception = false): bool { - if ($tick->getStatus() !== 'run') { - if ($exception) { - throw new TickIsNotAliveException(); - } else { - return false; - } - } - - return true; - } - - - /** - * @return bool - */ - public function isAlreadyRunning(): bool { - $ttl = $this->appConfig->getAppValueInt(ConfigLexicon::TICK_TTL); - $ticks = $this->tickRequest->getTicksByStatus('run'); - - $isAlreadyRunning = false; - foreach ($ticks as $tick) { - if ($tick->getTick() < (time() - $ttl)) { - $tick->setStatus('timeout'); - $this->tickRequest->update($tick); - } else { - $isAlreadyRunning = true; - } - } - - return $isAlreadyRunning; - } - - - /** - * - */ - public function forceStop() { - $ticks = $this->tickRequest->getTicksByStatus('run'); - - foreach ($ticks as $tick) { - $tick->setStatus('forceStop'); - $this->tickRequest->update($tick); + $this->lockService->update(); + } catch (LockException $e) { + throw new RunnerAlreadyUpException($e->getMessage()); } } - /** - * @param Tick $tick - * @param string $action + * @deprecated */ - private function assignActionToTick(Tick $tick, string $action) { - $now = microtime(true); - $preAction = $tick->getAction(); - - if ($preAction !== '') { - $preActionTotal = $tick->getInfoFloat($preAction . 'Total', 0); - $preActionStart = $tick->getInfoFloat($preAction . 'Init', 0); - - if ($preActionStart > 0) { - - $preActionTotal += ($now - $preActionStart); - $tick->setInfoFloat($preAction . 'Total', $preActionTotal); - $tick->unsetInfo($preAction . 'Init'); - } - } - $tick->setAction($action) - ->setInfoFloat($action . 'Init', $now); + public function stop(int $runId, string $reason = '') { + $this->lockService->unlock(); } - - } diff --git a/screenshots/0.3.0.png b/screenshots/0.3.0.png deleted file mode 100644 index 93b41074..00000000 Binary files a/screenshots/0.3.0.png and /dev/null differ diff --git a/screenshots/OccFulltextsearchIndex.png b/screenshots/OccFulltextsearchIndex.png deleted file mode 100644 index 3ed16dc6..00000000 Binary files a/screenshots/OccFulltextsearchIndex.png and /dev/null differ diff --git a/screenshots/OccFulltextsearchTest.png b/screenshots/OccFulltextsearchTest.png deleted file mode 100644 index c3091bd9..00000000 Binary files a/screenshots/OccFulltextsearchTest.png and /dev/null differ diff --git a/screenshots/SchemaIndexCommand.jpg b/screenshots/SchemaIndexCommand.jpg deleted file mode 100644 index a7b29140..00000000 Binary files a/screenshots/SchemaIndexCommand.jpg and /dev/null differ diff --git a/screenshots/SchemaIndexLive.jpg b/screenshots/SchemaIndexLive.jpg deleted file mode 100644 index 066b6aff..00000000 Binary files a/screenshots/SchemaIndexLive.jpg and /dev/null differ diff --git a/screenshots/SchemaSearch.jpg b/screenshots/SchemaSearch.jpg deleted file mode 100644 index 0a0a51cc..00000000 Binary files a/screenshots/SchemaSearch.jpg and /dev/null differ diff --git a/screenshots/configuration.png b/screenshots/configuration.png deleted file mode 100644 index 94289796..00000000 Binary files a/screenshots/configuration.png and /dev/null differ diff --git a/screenshots/draw.io/Index.xml b/screenshots/draw.io/Index.xml deleted file mode 100644 index 79b8005c..00000000 --- a/screenshots/draw.io/Index.xml +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/screenshots/draw.io/Search.xml b/screenshots/draw.io/Search.xml deleted file mode 100644 index 3dd4eca6..00000000 --- a/screenshots/draw.io/Search.xml +++ /dev/null @@ -1 +0,0 @@ -7Vpbb6M4FP41eWxkc89jm7azI81KVTvSdh5d4ibsAM4Yp0n3168NNmDsUJqQprsaRprAMRyb833nZjpx59nuC0Xr1Z9kgdOJAxa7iXs9cRzoAsh/hOS1kgTQqwRLmizkTY3gIfkHSyGQ0k2ywIV2IyMkZclaF8Ykz3HMNBmilGz1255Jqs+6RktsCB5ilJrSv5IFW1XSyAeN/A+cLFdqZgjkyBOKfy4p2eRyvonjPpdHNZwhpUveX6zQgmxbIvdm4s4pIaw6y3ZznArbKrNVz93uGY2CSu0LSjfyVa6aFTnAUeunOGeDFM4MhXLh7FUZq1SOxQNw4l5tVwnDD2sUi9EtpweXrViWyuGCUfITz0lKaPm0OysPPvKcpGlLfhOIf1xuLrl+S0wZ3rUQk+/wBZMMM/rKb5GjkTS3pKOy/rbB1p1J2aqFqxNKIZJ8WtaaG5vxE2k2uwmV4tFMuM9Q+007hgl9oNvQhRYjejYjAn8EI8L/pRFtRLTacBQiOhYbBikT1iD8ndrGDH5tiBq4KMr4fCnih7feNYP8bCl+bzflPN/xTih5wIjGK6WZL6pSXt2qxE+0kXRgZEJNT8zISY47AEoRSpNlzi9jjhDm8isBTMKj+qUcyJLFQkxjJUdDHzAS2tDzNbTVZQttJ7B6zAhguycCe85vwqWCO0peeJqmv6Hmju2B80HtnQhq5cngLkXsmdDsN9Icab+TB93AhFrBPzbU0BmQB3G+uBQ1sDBaiooiiUtzI8pMcRsQVYpayzRQHnwE7xL2KO8S5z+Ebad+bV28MEpr09wF2dC4rvnDSshXuMSqoq6KBhOFlpV9iz8pGcWcssmLvhKb5eUMdyQpPUWC7AV65IZOOA28EPhR5Lg+D64zXWP1PlJJu5zu6PWhrjfsV1tZxFBbsqS2yUDimJ3BMOIcwJCcL+uxpEXoq+sfLco8SncchT6zyMae4JzscfQIEXY9fyhbQl1P0C0Cx6RHaLKBI/EgL2Us1qNpixk1qhJxCSzU2QBDt02HCzAF9R13mCZ8wSLCl8o1rv2NGXuVuwRowwgXEcpWZElylH4jZP0mJ8eMTRXjTHINZs3waB993mjf4NrG9MhMEFisHZ41ETjeVAZnD8IwCPS0ELhTtxn0wk6nOzgt+J10EwbTIIqg5828yAOegv0Eju+aBcX9zcN3LrnHvza4YAbh9CAw1nbPbXnU3DHqMRt79jdeeuCEljYb2trsLn4HOa1ra7NPlGmt3un0uWd4jH8qsui59qwOGuhQX7gH5lrY4cxFdLpk63rjJNse1KEF9c+SUi1BXjLrA3JqFBremXAz75J8aa6/f298ZqoSQa3UBOqeWLXEX8Us1yTeZOXuSYAyESrzp2Ld6qD5ZPUD24SJ3nuBGUrS4sgw3IbegpN6k3d0weG0k7MiM8g6rsXxA+d4FGdmYbTEOaacK3bry09Ut9WWxj0uNimzbVuYMPC1IQEDKn9Q+RmFyufFO+xBci/uxgQDdk/OjHVnb8tV1n9rcyvyRvBXE+k5JXmJ8S3//5sIDg4orbzPgcFLUiRPypj9Tg2hmb7b1Oqgy29U1LIC3c+tZ0qyvbHCzpVTMiN6LzO6hVaNeJsZsCf5H1VoQdsW9zGFVm/NZK+1WuWUzZRa4lPM6qTM8+1xdcA7tHTi3cvUax2urjZ0pk77OFlZpfbWTtktg4E7X2ZP3FOr7+ePViPB8FPxB/oz3ho3x6wD+4Fs6rAy7IaKMQkTmPw4aNOrZ89L2wAFXSqNVYW/HXtse+yDA9L4O1vQrJ3FuhxANoyXWtzcb9VVWtrdU1fZNdQpeS0/YH7ls81FjSeTt7j+2Jrs+MwLLR8cT5Z5gfnFcU6yrCqOe0uxvWUXMIN3kpUA2Qt6VcjLPSob/OAJFYJIoCwX4xUhBRZnZM0Skp+2o2pM9I6WSm+oVED9iH6qbsot35AH9VD9DQ1bIeHbMcl5Lyssv684tnt3XVT7VxP/etiUfb4trvGOURRb6/TPRQsHdveGLX22zdO7G1iH8cIWp1vd0LAYPcBZ/2swzAaicEC85ZfNH4BWdU3zV7buzb8= \ No newline at end of file