Skip to content

Commit f7c552e

Browse files
authored
Merge pull request #7653 from nextcloud/fix/steps-cleanup-orphaned
Fix: Add cleanup for orphaned text steps
2 parents cff0bfb + 386b4c6 commit f7c552e

File tree

4 files changed

+117
-1
lines changed

4 files changed

+117
-1
lines changed

lib/Cron/Cleanup.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ protected function run($argument): void {
5858
$removedOldSessions = $this->sessionService->removeOldSessions();
5959
$this->logger->debug('Removed ' . $removedOldSessions . ' old sessions');
6060

61+
$this->logger->debug('Run cleanup job for orphaned steps');
62+
$removedSteps = $this->sessionService->removeOrphanedSteps();
63+
$this->logger->debug('Removed ' . $removedSteps . ' orphaned steps');
64+
6165
$this->logger->debug('Run cleanup job for obsolete documents folders');
6266
$this->documentService->cleanupOldDocumentsFolders();
6367
}

lib/Db/SessionMapper.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,51 @@ public function deleteOldSessions(int $ageInSeconds): int {
178178
return $deletedCount;
179179
}
180180

181+
public function deleteOrphanedSteps(int $ageInSeconds): int {
182+
$startTime = microtime(true);
183+
$maxExecutionSeconds = 30;
184+
$batchSize = 1000;
185+
$deletedCount = 0;
186+
$ageThreshold = time() - $ageInSeconds;
187+
188+
do {
189+
$orphanedStepsQb = $this->db->getQueryBuilder();
190+
$orphanedStepsQb->select('st.id')
191+
->from('text_steps', 'st')
192+
->leftJoin('st', 'text_sessions', 's', $orphanedStepsQb->expr()->eq('st.document_id', 's.document_id'))
193+
->leftJoin('st', 'text_documents', 'd', $orphanedStepsQb->expr()->eq('st.document_id', 'd.id'))
194+
->where($orphanedStepsQb->expr()->isNull('s.id'))
195+
->andWhere($orphanedStepsQb->expr()->lt('st.timestamp', $orphanedStepsQb->createNamedParameter($ageThreshold)))
196+
->andWhere(
197+
$orphanedStepsQb->expr()->orX(
198+
$orphanedStepsQb->expr()->isNull('d.id'),
199+
$orphanedStepsQb->expr()->lt('st.id', 'd.last_saved_version')
200+
)
201+
)
202+
->setMaxResults($batchSize);
203+
204+
$result = $orphanedStepsQb->executeQuery();
205+
$stepIds = array_map(function ($row) {
206+
return (int)$row['id'];
207+
}, $result->fetchAll());
208+
$result->closeCursor();
209+
210+
if (empty($stepIds)) {
211+
break;
212+
}
213+
214+
$deleteQb = $this->db->getQueryBuilder();
215+
$batchDeleted = $deleteQb->delete('text_steps')
216+
->where($deleteQb->expr()->in('id', $deleteQb->createParameter('ids'), IQueryBuilder::PARAM_INT_ARRAY))
217+
->setParameter('ids', $stepIds, IQueryBuilder::PARAM_INT_ARRAY)
218+
->executeStatement();
219+
220+
$deletedCount += $batchDeleted;
221+
} while ((microtime(true) - $startTime) < $maxExecutionSeconds);
222+
223+
return $deletedCount;
224+
}
225+
181226
public function deleteByDocumentId(int $documentId): int {
182227
$qb = $this->db->getQueryBuilder();
183228
$qb->delete($this->getTableName())

lib/Service/SessionService.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ public function removeOldSessions(int $ageInSeconds = 7776000): int {
142142
return $this->sessionMapper->deleteOldSessions($ageInSeconds);
143143
}
144144

145+
public function removeOrphanedSteps(int $ageInSeconds = 604800): int {
146+
return $this->sessionMapper->deleteOrphanedSteps($ageInSeconds);
147+
}
148+
145149
public function getSession(int $documentId, int $sessionId, string $token): ?Session {
146150
if ($this->session !== null) {
147151
return $this->session;

tests/unit/Db/SessionMapperTest.php

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ class SessionMapperTest extends \Test\TestCase {
99

1010
private SessionMapper $sessionMapper;
1111
private StepMapper $stepMapper;
12+
private DocumentMapper $documentMapper;
1213

1314
public function setUp(): void {
1415
parent::setUp();
1516
$this->sessionMapper = \OCP\Server::get(SessionMapper::class);
1617
$this->stepMapper = \OCP\Server::get(StepMapper::class);
17-
18+
$this->documentMapper = \OCP\Server::get(DocumentMapper::class);
1819
}
1920

2021
public function testDeleteInactiveWithoutSteps() {
@@ -135,4 +136,66 @@ public function testDeleteOldSessions() {
135136
self::assertCount(1, $remainingSessions);
136137
self::assertEquals($recentSession->getId(), $remainingSessions[0]->getId());
137138
}
139+
140+
public function testDeleteOrphanedSteps() {
141+
$this->documentMapper->clearAll();
142+
$this->sessionMapper->clearAll();
143+
$this->stepMapper->clearAll();
144+
145+
$eightDaysAgo = time() - (8 * 24 * 60 * 60);
146+
147+
// Create document
148+
$document = $this->documentMapper->insert(Document::fromParams([
149+
'id' => 1,
150+
'currentVersion' => 0,
151+
'lastSavedVersion' => 100,
152+
'lastSavedVersionTime' => time()
153+
]));
154+
155+
// Create Orphaned step without document (delete)
156+
$this->stepMapper->insert(Step::fromParams([
157+
'sessionId' => 99999,
158+
'documentId' => 99999,
159+
'data' => 'ORPHANED_NO_DOC',
160+
'version' => 1
161+
]));
162+
163+
// Orphaned "old" step with document and old version (delete)
164+
$this->stepMapper->insert(Step::fromParams([
165+
'id' => 1,
166+
'sessionId' => 99999,
167+
'documentId' => $document->getId(),
168+
'data' => 'ORPHANED_OLD_VERSION',
169+
'timestamp' => $eightDaysAgo,
170+
'version' => 1
171+
]));
172+
173+
// Orphaned "new" step with document and current version (keep)
174+
$this->stepMapper->insert(Step::fromParams([
175+
'id' => 100,
176+
'sessionId' => 99999,
177+
'documentId' => $document->getId(),
178+
'data' => 'ORPHANED_CURRENT_VERSION',
179+
'version' => 2
180+
]));
181+
182+
// Orphaned step with document and new version (keep)
183+
$this->stepMapper->insert(Step::fromParams([
184+
'id' => 101,
185+
'sessionId' => 99999,
186+
'documentId' => $document->getId(),
187+
'data' => 'ORPHANED_NEW_VERSION',
188+
'timestamp' => $eightDaysAgo,
189+
'version' => 3
190+
]));
191+
192+
// Verify steps for document 1 and 99999
193+
self::assertCount(3, $this->stepMapper->find(1, 0));
194+
self::assertCount(1, $this->stepMapper->find(99999, 0));
195+
196+
// Delete orphaned steps older than 7 days
197+
$sevenDays = 7 * 24 * 60 * 60;
198+
$deletedCount = $this->sessionMapper->deleteOrphanedSteps($sevenDays);
199+
self::assertEquals(2, $deletedCount);
200+
}
138201
}

0 commit comments

Comments
 (0)