Skip to content

Commit bf4acd5

Browse files
Merge pull request #31106 from nextcloud/techdebt/noid/improve-user-status-update-handling
Improve user status revert performance
2 parents 98fd66b + 058d1de commit bf4acd5

File tree

8 files changed

+391
-77
lines changed

8 files changed

+391
-77
lines changed

apps/user_status/lib/Connector/UserStatusProvider.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,17 +57,15 @@ public function getUserStatuses(array $userIds): array {
5757
return $userStatuses;
5858
}
5959

60-
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false): void {
61-
if ($createBackup) {
62-
if ($this->service->backupCurrentStatus($userId) === false) {
63-
return; // Already a status set automatically => abort.
64-
}
65-
}
66-
$this->service->setStatus($userId, $status, null, true);
67-
$this->service->setPredefinedMessage($userId, $messageId, null);
60+
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void {
61+
$this->service->setUserStatus($userId, $status, $messageId, $createBackup);
6862
}
6963

7064
public function revertUserStatus(string $userId, string $messageId, string $status): void {
7165
$this->service->revertUserStatus($userId, $messageId, $status);
7266
}
67+
68+
public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
69+
$this->service->revertMultipleUserStatus($userIds, $messageId, $status);
70+
}
7371
}

apps/user_status/lib/Db/UserStatusMapper.php

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,7 @@ public function findByUserId(string $userId, bool $isBackup = false):UserStatus
101101
$qb
102102
->select('*')
103103
->from($this->tableName)
104-
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($isBackup ? '_' . $userId : $userId, IQueryBuilder::PARAM_STR)))
105-
->andWhere($qb->expr()->eq('is_backup', $qb->createNamedParameter($isBackup, IQueryBuilder::PARAM_BOOL)));
104+
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($isBackup ? '_' . $userId : $userId, IQueryBuilder::PARAM_STR)));
106105

107106
return $this->findEntity($qb);
108107
}
@@ -111,7 +110,7 @@ public function findByUserId(string $userId, bool $isBackup = false):UserStatus
111110
* @param array $userIds
112111
* @return array
113112
*/
114-
public function findByUserIds(array $userIds):array {
113+
public function findByUserIds(array $userIds): array {
115114
$qb = $this->db->getQueryBuilder();
116115
$qb
117116
->select('*')
@@ -158,4 +157,57 @@ public function clearMessagesOlderThan(int $timestamp): void {
158157

159158
$qb->execute();
160159
}
160+
161+
162+
/**
163+
* Deletes a user status so we can restore the backup
164+
*
165+
* @param string $userId
166+
* @param string $messageId
167+
* @param string $status
168+
* @return bool True if an entry was deleted
169+
*/
170+
public function deleteCurrentStatusToRestoreBackup(string $userId, string $messageId, string $status): bool {
171+
$qb = $this->db->getQueryBuilder();
172+
$qb->delete($this->tableName)
173+
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
174+
->andWhere($qb->expr()->eq('message_id', $qb->createNamedParameter($messageId)))
175+
->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($status)))
176+
->andWhere($qb->expr()->eq('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)));
177+
return $qb->executeStatement() > 0;
178+
}
179+
180+
public function deleteByIds(array $ids): void {
181+
$qb = $this->db->getQueryBuilder();
182+
$qb->delete($this->tableName)
183+
->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
184+
$qb->executeStatement();
185+
}
186+
187+
/**
188+
* @param string $userId
189+
* @return bool
190+
* @throws \OCP\DB\Exception
191+
*/
192+
public function createBackupStatus(string $userId): bool {
193+
// Prefix user account with an underscore because user_id is marked as unique
194+
// in the table. Starting a username with an underscore is not allowed so this
195+
// shouldn't create any trouble.
196+
$qb = $this->db->getQueryBuilder();
197+
$qb->update($this->tableName)
198+
->set('is_backup', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
199+
->set('user_id', $qb->createNamedParameter('_' . $userId))
200+
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
201+
return $qb->executeStatement() > 0;
202+
}
203+
204+
public function restoreBackupStatuses(array $ids): void {
205+
$qb = $this->db->getQueryBuilder();
206+
$qb->update($this->tableName)
207+
->set('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))
208+
->set('user_id', $qb->func()->substring('user_id', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT)))
209+
->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
210+
211+
$qb->executeStatement();
212+
}
161213
}

apps/user_status/lib/Service/StatusService.php

Lines changed: 104 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@
3535
use OCA\UserStatus\Exception\StatusMessageTooLongException;
3636
use OCP\AppFramework\Db\DoesNotExistException;
3737
use OCP\AppFramework\Utility\ITimeFactory;
38+
use OCP\DB\Exception;
3839
use OCP\IConfig;
40+
use OCP\IUser;
3941
use OCP\UserStatus\IUserStatus;
4042

4143
/**
@@ -229,6 +231,7 @@ public function setPredefinedMessage(string $userId,
229231
$userStatus->setStatus(IUserStatus::OFFLINE);
230232
$userStatus->setStatusTimestamp(0);
231233
$userStatus->setIsUserDefined(false);
234+
$userStatus->setIsBackup(false);
232235
}
233236

234237
if (!$this->predefinedStatusService->isValidId($messageId)) {
@@ -252,6 +255,60 @@ public function setPredefinedMessage(string $userId,
252255
return $this->mapper->update($userStatus);
253256
}
254257

258+
/**
259+
* @param string $userId
260+
* @param string $status
261+
* @param string $messageId
262+
* @param bool $createBackup
263+
* @throws InvalidStatusTypeException
264+
* @throws InvalidMessageIdException
265+
*/
266+
public function setUserStatus(string $userId,
267+
string $status,
268+
string $messageId,
269+
bool $createBackup): void {
270+
// Check if status-type is valid
271+
if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
272+
throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
273+
}
274+
275+
if (!$this->predefinedStatusService->isValidId($messageId)) {
276+
throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
277+
}
278+
279+
if ($createBackup) {
280+
if ($this->backupCurrentStatus($userId) === false) {
281+
return; // Already a status set automatically => abort.
282+
}
283+
284+
// If we just created the backup
285+
$userStatus = new UserStatus();
286+
$userStatus->setUserId($userId);
287+
} else {
288+
try {
289+
$userStatus = $this->mapper->findByUserId($userId);
290+
} catch (DoesNotExistException $ex) {
291+
$userStatus = new UserStatus();
292+
$userStatus->setUserId($userId);
293+
}
294+
}
295+
296+
$userStatus->setStatus($status);
297+
$userStatus->setStatusTimestamp($this->timeFactory->getTime());
298+
$userStatus->setIsUserDefined(false);
299+
$userStatus->setIsBackup(false);
300+
$userStatus->setMessageId($messageId);
301+
$userStatus->setCustomIcon(null);
302+
$userStatus->setCustomMessage(null);
303+
$userStatus->setClearAt(null);
304+
305+
if ($userStatus->getId() !== null) {
306+
$this->mapper->update($userStatus);
307+
return;
308+
}
309+
$this->mapper->insert($userStatus);
310+
}
311+
255312
/**
256313
* @param string $userId
257314
* @param string|null $statusIcon
@@ -434,53 +491,71 @@ private function addDefaultMessage(UserStatus $status): void {
434491
}
435492

436493
/**
437-
* @return bool false iff there is already a backup. In this case abort the procedure.
494+
* @return bool false if there is already a backup. In this case abort the procedure.
438495
*/
439496
public function backupCurrentStatus(string $userId): bool {
440497
try {
441-
$this->mapper->findByUserId($userId, true);
442-
return false;
443-
} catch (DoesNotExistException $ex) {
444-
// No backup already existing => Good
445-
}
446-
447-
try {
448-
$userStatus = $this->mapper->findByUserId($userId);
449-
} catch (DoesNotExistException $ex) {
450-
// if there is no status to backup, just return
498+
$this->mapper->createBackupStatus($userId);
451499
return true;
500+
} catch (Exception $ex) {
501+
if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
502+
return false;
503+
}
504+
throw $ex;
452505
}
453-
454-
$userStatus->setIsBackup(true);
455-
// Prefix user account with an underscore because user_id is marked as unique
456-
// in the table. Starting an username with an underscore is not allowed so this
457-
// shouldn't create any trouble.
458-
$userStatus->setUserId('_' . $userStatus->getUserId());
459-
$this->mapper->update($userStatus);
460-
return true;
461506
}
462507

463-
public function revertUserStatus(string $userId, ?string $messageId, string $status): void {
508+
public function revertUserStatus(string $userId, string $messageId, string $status): void {
464509
try {
465510
/** @var UserStatus $userStatus */
466511
$backupUserStatus = $this->mapper->findByUserId($userId, true);
467512
} catch (DoesNotExistException $ex) {
468513
// No user status to revert, do nothing
469514
return;
470515
}
471-
try {
472-
$userStatus = $this->mapper->findByUserId($userId);
473-
if ($userStatus->getMessageId() !== $messageId || $userStatus->getStatus() !== $status) {
474-
// Another status is set automatically, do nothing
475-
return;
476-
}
477-
$this->removeUserStatus($userId);
478-
} catch (DoesNotExistException $ex) {
479-
// No current status => nothing to delete
516+
517+
$deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId, $status);
518+
if (!$deleted) {
519+
// Another status is set automatically or no status, do nothing
520+
return;
480521
}
522+
481523
$backupUserStatus->setIsBackup(false);
482524
// Remove the underscore prefix added when creating the backup
483525
$backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1));
484526
$this->mapper->update($backupUserStatus);
485527
}
528+
529+
public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
530+
// Get all user statuses and the backups
531+
$findById = $userIds;
532+
foreach ($userIds as $userId) {
533+
$findById[] = '_' . $userId;
534+
}
535+
$userStatuses = $this->mapper->findByUserIds($findById);
536+
537+
$backups = $restoreIds = $statuesToDelete = [];
538+
foreach ($userStatuses as $userStatus) {
539+
if (!$userStatus->getIsBackup()
540+
&& $userStatus->getMessageId() === $messageId
541+
&& $userStatus->getStatus() === $status) {
542+
$statuesToDelete[$userStatus->getUserId()] = $userStatus->getId();
543+
} else if ($userStatus->getIsBackup()) {
544+
$backups[$userStatus->getUserId()] = $userStatus->getId();
545+
}
546+
}
547+
548+
// For users with both (normal and backup) delete the status when matching
549+
foreach ($statuesToDelete as $userId => $statusId) {
550+
$backupUserId = '_' . $userId;
551+
if (isset($backups[$backupUserId])) {
552+
$restoreIds[] = $backups[$backupUserId];
553+
}
554+
}
555+
556+
$this->mapper->deleteByIds(array_values($statuesToDelete));
557+
558+
// For users that matched restore the previous status
559+
$this->mapper->restoreBackupStatuses($restoreIds);
560+
}
486561
}

apps/user_status/tests/Unit/Db/UserStatusMapperTest.php

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,117 @@ private function insertSampleStatuses(): void {
251251
$this->mapper->insert($userStatus2);
252252
$this->mapper->insert($userStatus3);
253253
}
254+
255+
public function dataCreateBackupStatus(): array {
256+
return [
257+
[false, false, false],
258+
[true, false, true],
259+
[false, true, false],
260+
[true, true, false],
261+
];
262+
}
263+
264+
/**
265+
* @dataProvider dataCreateBackupStatus
266+
* @param bool $hasStatus
267+
* @param bool $hasBackup
268+
* @param bool $backupCreated
269+
*/
270+
public function testCreateBackupStatus(bool $hasStatus, bool $hasBackup, bool $backupCreated): void {
271+
if ($hasStatus) {
272+
$userStatus1 = new UserStatus();
273+
$userStatus1->setUserId('user1');
274+
$userStatus1->setStatus('online');
275+
$userStatus1->setStatusTimestamp(5000);
276+
$userStatus1->setIsUserDefined(true);
277+
$userStatus1->setIsBackup(false);
278+
$userStatus1->setCustomIcon('🚀');
279+
$userStatus1->setCustomMessage('Current');
280+
$userStatus1->setClearAt(50000);
281+
$this->mapper->insert($userStatus1);
282+
}
283+
284+
if ($hasBackup) {
285+
$userStatus1 = new UserStatus();
286+
$userStatus1->setUserId('_user1');
287+
$userStatus1->setStatus('online');
288+
$userStatus1->setStatusTimestamp(5000);
289+
$userStatus1->setIsUserDefined(true);
290+
$userStatus1->setIsBackup(true);
291+
$userStatus1->setCustomIcon('🚀');
292+
$userStatus1->setCustomMessage('Backup');
293+
$userStatus1->setClearAt(50000);
294+
$this->mapper->insert($userStatus1);
295+
}
296+
297+
if ($hasStatus && $hasBackup) {
298+
$this->expectException(Exception::class);
299+
}
300+
301+
self::assertSame($backupCreated, $this->mapper->createBackupStatus('user1'));
302+
303+
if ($backupCreated) {
304+
$user1Status = $this->mapper->findByUserId('user1', true);
305+
$this->assertEquals('_user1', $user1Status->getUserId());
306+
$this->assertEquals(true, $user1Status->getIsBackup());
307+
$this->assertEquals('Current', $user1Status->getCustomMessage());
308+
} else if ($hasBackup) {
309+
$user1Status = $this->mapper->findByUserId('user1', true);
310+
$this->assertEquals('_user1', $user1Status->getUserId());
311+
$this->assertEquals(true, $user1Status->getIsBackup());
312+
$this->assertEquals('Backup', $user1Status->getCustomMessage());
313+
}
314+
}
315+
316+
public function testRestoreBackupStatuses(): void {
317+
$userStatus1 = new UserStatus();
318+
$userStatus1->setUserId('_user1');
319+
$userStatus1->setStatus('online');
320+
$userStatus1->setStatusTimestamp(5000);
321+
$userStatus1->setIsUserDefined(true);
322+
$userStatus1->setIsBackup(true);
323+
$userStatus1->setCustomIcon('🚀');
324+
$userStatus1->setCustomMessage('Releasing');
325+
$userStatus1->setClearAt(50000);
326+
$userStatus1 = $this->mapper->insert($userStatus1);
327+
328+
$userStatus2 = new UserStatus();
329+
$userStatus2->setUserId('_user2');
330+
$userStatus2->setStatus('away');
331+
$userStatus2->setStatusTimestamp(5000);
332+
$userStatus2->setIsUserDefined(true);
333+
$userStatus2->setIsBackup(true);
334+
$userStatus2->setCustomIcon('💩');
335+
$userStatus2->setCustomMessage('Do not disturb');
336+
$userStatus2->setClearAt(50000);
337+
$userStatus2 = $this->mapper->insert($userStatus2);
338+
339+
$userStatus3 = new UserStatus();
340+
$userStatus3->setUserId('_user3');
341+
$userStatus3->setStatus('away');
342+
$userStatus3->setStatusTimestamp(5000);
343+
$userStatus3->setIsUserDefined(true);
344+
$userStatus3->setIsBackup(true);
345+
$userStatus3->setCustomIcon('🏝️');
346+
$userStatus3->setCustomMessage('Vacationing');
347+
$userStatus3->setClearAt(50000);
348+
$this->mapper->insert($userStatus3);
349+
350+
$this->mapper->restoreBackupStatuses([$userStatus1->getId(), $userStatus2->getId()]);
351+
352+
$user1Status = $this->mapper->findByUserId('user1', false);
353+
$this->assertEquals('user1', $user1Status->getUserId());
354+
$this->assertEquals(false, $user1Status->getIsBackup());
355+
$this->assertEquals('Releasing', $user1Status->getCustomMessage());
356+
357+
$user2Status = $this->mapper->findByUserId('user2', false);
358+
$this->assertEquals('user2', $user2Status->getUserId());
359+
$this->assertEquals(false, $user2Status->getIsBackup());
360+
$this->assertEquals('Do not disturb', $user2Status->getCustomMessage());
361+
362+
$user3Status = $this->mapper->findByUserId('user3', true);
363+
$this->assertEquals('_user3', $user3Status->getUserId());
364+
$this->assertEquals(true, $user3Status->getIsBackup());
365+
$this->assertEquals('Vacationing', $user3Status->getCustomMessage());
366+
}
254367
}

0 commit comments

Comments
 (0)