Skip to content

Commit 8a30eae

Browse files
committed
feat: Implement background job to remove unused models
Signed-off-by: David Dreschner <david.dreschner@nextcloud.com>
1 parent 1b69943 commit 8a30eae

5 files changed

Lines changed: 403 additions & 0 deletions

File tree

appinfo/info.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<job>OCA\SuspiciousLogin\BackgroundJob\ETLJob</job>
3333
<job>OCA\SuspiciousLogin\BackgroundJob\TrainJobIpV4</job>
3434
<job>OCA\SuspiciousLogin\BackgroundJob\TrainJobIpV6</job>
35+
<job>OCA\SuspiciousLogin\BackgroundJob\CleanupModelsJob</job>
3536
</background-jobs>
3637

3738
<commands>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\SuspiciousLogin\BackgroundJob;
11+
12+
use OCA\SuspiciousLogin\Service\ModelStore;
13+
use OCP\AppFramework\Utility\ITimeFactory;
14+
use OCP\BackgroundJob\TimedJob;
15+
use Psr\Log\LoggerInterface;
16+
use Throwable;
17+
18+
class CleanupModelsJob extends TimedJob {
19+
20+
/**
21+
* @var int Number of most recent models to keep per address type.
22+
* Should match the number of models consumed by the statistics
23+
* API.
24+
*/
25+
private const MODELS_TO_KEEP = 14;
26+
27+
public function __construct(
28+
private readonly ModelStore $modelStore,
29+
private readonly LoggerInterface $logger,
30+
ITimeFactory $time,
31+
) {
32+
parent::__construct($time);
33+
34+
// Run once per day
35+
$this->setInterval(24 * 60 * 60);
36+
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
37+
$this->allowParallelRuns = false;
38+
}
39+
40+
#[\Override]
41+
protected function run($argument): void {
42+
try {
43+
$this->modelStore->cleanup(self::MODELS_TO_KEEP);
44+
} catch (Throwable $e) {
45+
$this->logger->error('Error during model cleanup: ' . $e->getMessage(), [
46+
'exception' => $e,
47+
]);
48+
}
49+
}
50+
}

lib/Db/ModelMapper.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,20 @@ public function findMostRecent(int $max, string $addressType): array {
6666

6767
return $this->findEntities($query);
6868
}
69+
70+
/**
71+
* Find all models older than `$numberOfModelsToKeep` per `$addressType`
72+
*
73+
* @return Model[]
74+
*/
75+
public function findOld(int $numberOfModelsToKeep, string $addressType): array {
76+
$qb = $this->db->getQueryBuilder();
77+
$query = $qb->select('*')
78+
->from($this->getTableName())
79+
->where($qb->expr()->eq('address_type', $qb->createNamedParameter($addressType)))
80+
->setFirstResult($numberOfModelsToKeep)
81+
->orderBy('created_at', 'desc');
82+
83+
return $this->findEntities($query);
84+
}
6985
}

lib/Service/ModelStore.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,59 @@ public function persist(Learner $estimator, Model $model) {
165165
throw $e;
166166
}
167167
}
168+
169+
/**
170+
* Remove old models, keeping only the `$numberOfModelsToKeep` ones
171+
* for each used address type (IPv4 / IPv6).
172+
*
173+
* @throws \OCP\DB\Exception
174+
* @throws \OCP\Files\NotPermittedException
175+
*/
176+
public function cleanup(int $numberOfModelsToKeep): void {
177+
$oldModels = array_merge(
178+
$this->modelMapper->findOld($numberOfModelsToKeep, Ipv4Strategy::getTypeName()),
179+
$this->modelMapper->findOld($numberOfModelsToKeep, IpV6Strategy::getTypeName())
180+
);
181+
182+
if (empty($oldModels)) {
183+
$this->logger->debug('No old models to clean up');
184+
return;
185+
}
186+
187+
try {
188+
$modelsFolder = $this->appData->getFolder(self::APPDATA_MODELS_FOLDER);
189+
} catch (NotFoundException) {
190+
$this->logger->debug('Models folder does not exist, skipping file deletion');
191+
}
192+
193+
if ($this->cacheFactory->isLocalCacheAvailable()) {
194+
$cache = $this->cacheFactory->createLocal();
195+
}
196+
197+
foreach ($oldModels as $oldModel) {
198+
$id = $oldModel->getId();
199+
$this->logger->debug("Cleaning up old model $id");
200+
201+
// Remove the model file from app data
202+
if (isset($modelsFolder)) {
203+
try {
204+
$modelFile = $modelsFolder->getFile((string)$id);
205+
$modelFile->delete();
206+
} catch (NotFoundException) {
207+
$this->logger->debug("Model file $id not found in app data, skipping file deletion");
208+
}
209+
}
210+
211+
// Evict from cache
212+
if (isset($cache)) {
213+
$cache->remove($this->getCacheKey($id));
214+
}
215+
216+
// Remove the DB record
217+
$this->modelMapper->delete($oldModel);
218+
}
219+
220+
$numberOfOldModels = count($oldModels);
221+
$this->logger->info("Cleaned up {$numberOfOldModels} old model(s)");
222+
}
168223
}

0 commit comments

Comments
 (0)