Skip to content

Commit

Permalink
Merge pull request nextcloud#31106 from nextcloud/techdebt/noid/impro…
Browse files Browse the repository at this point in the history
…ve-user-status-update-handling

Improve user status revert performance
  • Loading branch information
nickvergessen authored Feb 23, 2022
2 parents 98fd66b + 058d1de commit bf4acd5
Show file tree
Hide file tree
Showing 8 changed files with 391 additions and 77 deletions.
14 changes: 6 additions & 8 deletions apps/user_status/lib/Connector/UserStatusProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,15 @@ public function getUserStatuses(array $userIds): array {
return $userStatuses;
}

public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false): void {
if ($createBackup) {
if ($this->service->backupCurrentStatus($userId) === false) {
return; // Already a status set automatically => abort.
}
}
$this->service->setStatus($userId, $status, null, true);
$this->service->setPredefinedMessage($userId, $messageId, null);
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void {
$this->service->setUserStatus($userId, $status, $messageId, $createBackup);
}

public function revertUserStatus(string $userId, string $messageId, string $status): void {
$this->service->revertUserStatus($userId, $messageId, $status);
}

public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
$this->service->revertMultipleUserStatus($userIds, $messageId, $status);
}
}
58 changes: 55 additions & 3 deletions apps/user_status/lib/Db/UserStatusMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ public function findByUserId(string $userId, bool $isBackup = false):UserStatus
$qb
->select('*')
->from($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($isBackup ? '_' . $userId : $userId, IQueryBuilder::PARAM_STR)))
->andWhere($qb->expr()->eq('is_backup', $qb->createNamedParameter($isBackup, IQueryBuilder::PARAM_BOOL)));
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($isBackup ? '_' . $userId : $userId, IQueryBuilder::PARAM_STR)));

return $this->findEntity($qb);
}
Expand All @@ -111,7 +110,7 @@ public function findByUserId(string $userId, bool $isBackup = false):UserStatus
* @param array $userIds
* @return array
*/
public function findByUserIds(array $userIds):array {
public function findByUserIds(array $userIds): array {
$qb = $this->db->getQueryBuilder();
$qb
->select('*')
Expand Down Expand Up @@ -158,4 +157,57 @@ public function clearMessagesOlderThan(int $timestamp): void {

$qb->execute();
}


/**
* Deletes a user status so we can restore the backup
*
* @param string $userId
* @param string $messageId
* @param string $status
* @return bool True if an entry was deleted
*/
public function deleteCurrentStatusToRestoreBackup(string $userId, string $messageId, string $status): bool {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
->andWhere($qb->expr()->eq('message_id', $qb->createNamedParameter($messageId)))
->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($status)))
->andWhere($qb->expr()->eq('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)));
return $qb->executeStatement() > 0;
}

public function deleteByIds(array $ids): void {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
$qb->executeStatement();
}

/**
* @param string $userId
* @return bool
* @throws \OCP\DB\Exception
*/
public function createBackupStatus(string $userId): bool {
// Prefix user account with an underscore because user_id is marked as unique
// in the table. Starting a username with an underscore is not allowed so this
// shouldn't create any trouble.
$qb = $this->db->getQueryBuilder();
$qb->update($this->tableName)
->set('is_backup', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))
->set('user_id', $qb->createNamedParameter('_' . $userId))
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $qb->executeStatement() > 0;
}

public function restoreBackupStatuses(array $ids): void {
$qb = $this->db->getQueryBuilder();
$qb->update($this->tableName)
->set('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))
->set('user_id', $qb->func()->substring('user_id', $qb->createNamedParameter(2, IQueryBuilder::PARAM_INT)))
->where($qb->expr()->in('id', $qb->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));

$qb->executeStatement();
}
}
133 changes: 104 additions & 29 deletions apps/user_status/lib/Service/StatusService.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
use OCA\UserStatus\Exception\StatusMessageTooLongException;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\Exception;
use OCP\IConfig;
use OCP\IUser;
use OCP\UserStatus\IUserStatus;

/**
Expand Down Expand Up @@ -229,6 +231,7 @@ public function setPredefinedMessage(string $userId,
$userStatus->setStatus(IUserStatus::OFFLINE);
$userStatus->setStatusTimestamp(0);
$userStatus->setIsUserDefined(false);
$userStatus->setIsBackup(false);
}

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

/**
* @param string $userId
* @param string $status
* @param string $messageId
* @param bool $createBackup
* @throws InvalidStatusTypeException
* @throws InvalidMessageIdException
*/
public function setUserStatus(string $userId,
string $status,
string $messageId,
bool $createBackup): void {
// Check if status-type is valid
if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
}

if (!$this->predefinedStatusService->isValidId($messageId)) {
throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
}

if ($createBackup) {
if ($this->backupCurrentStatus($userId) === false) {
return; // Already a status set automatically => abort.
}

// If we just created the backup
$userStatus = new UserStatus();
$userStatus->setUserId($userId);
} else {
try {
$userStatus = $this->mapper->findByUserId($userId);
} catch (DoesNotExistException $ex) {
$userStatus = new UserStatus();
$userStatus->setUserId($userId);
}
}

$userStatus->setStatus($status);
$userStatus->setStatusTimestamp($this->timeFactory->getTime());
$userStatus->setIsUserDefined(false);
$userStatus->setIsBackup(false);
$userStatus->setMessageId($messageId);
$userStatus->setCustomIcon(null);
$userStatus->setCustomMessage(null);
$userStatus->setClearAt(null);

if ($userStatus->getId() !== null) {
$this->mapper->update($userStatus);
return;
}
$this->mapper->insert($userStatus);
}

/**
* @param string $userId
* @param string|null $statusIcon
Expand Down Expand Up @@ -434,53 +491,71 @@ private function addDefaultMessage(UserStatus $status): void {
}

/**
* @return bool false iff there is already a backup. In this case abort the procedure.
* @return bool false if there is already a backup. In this case abort the procedure.
*/
public function backupCurrentStatus(string $userId): bool {
try {
$this->mapper->findByUserId($userId, true);
return false;
} catch (DoesNotExistException $ex) {
// No backup already existing => Good
}

try {
$userStatus = $this->mapper->findByUserId($userId);
} catch (DoesNotExistException $ex) {
// if there is no status to backup, just return
$this->mapper->createBackupStatus($userId);
return true;
} catch (Exception $ex) {
if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
return false;
}
throw $ex;
}

$userStatus->setIsBackup(true);
// Prefix user account with an underscore because user_id is marked as unique
// in the table. Starting an username with an underscore is not allowed so this
// shouldn't create any trouble.
$userStatus->setUserId('_' . $userStatus->getUserId());
$this->mapper->update($userStatus);
return true;
}

public function revertUserStatus(string $userId, ?string $messageId, string $status): void {
public function revertUserStatus(string $userId, string $messageId, string $status): void {
try {
/** @var UserStatus $userStatus */
$backupUserStatus = $this->mapper->findByUserId($userId, true);
} catch (DoesNotExistException $ex) {
// No user status to revert, do nothing
return;
}
try {
$userStatus = $this->mapper->findByUserId($userId);
if ($userStatus->getMessageId() !== $messageId || $userStatus->getStatus() !== $status) {
// Another status is set automatically, do nothing
return;
}
$this->removeUserStatus($userId);
} catch (DoesNotExistException $ex) {
// No current status => nothing to delete

$deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId, $status);
if (!$deleted) {
// Another status is set automatically or no status, do nothing
return;
}

$backupUserStatus->setIsBackup(false);
// Remove the underscore prefix added when creating the backup
$backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1));
$this->mapper->update($backupUserStatus);
}

public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void {
// Get all user statuses and the backups
$findById = $userIds;
foreach ($userIds as $userId) {
$findById[] = '_' . $userId;
}
$userStatuses = $this->mapper->findByUserIds($findById);

$backups = $restoreIds = $statuesToDelete = [];
foreach ($userStatuses as $userStatus) {
if (!$userStatus->getIsBackup()
&& $userStatus->getMessageId() === $messageId
&& $userStatus->getStatus() === $status) {
$statuesToDelete[$userStatus->getUserId()] = $userStatus->getId();
} else if ($userStatus->getIsBackup()) {
$backups[$userStatus->getUserId()] = $userStatus->getId();
}
}

// For users with both (normal and backup) delete the status when matching
foreach ($statuesToDelete as $userId => $statusId) {
$backupUserId = '_' . $userId;
if (isset($backups[$backupUserId])) {
$restoreIds[] = $backups[$backupUserId];
}
}

$this->mapper->deleteByIds(array_values($statuesToDelete));

// For users that matched restore the previous status
$this->mapper->restoreBackupStatuses($restoreIds);
}
}
113 changes: 113 additions & 0 deletions apps/user_status/tests/Unit/Db/UserStatusMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,117 @@ private function insertSampleStatuses(): void {
$this->mapper->insert($userStatus2);
$this->mapper->insert($userStatus3);
}

public function dataCreateBackupStatus(): array {
return [
[false, false, false],
[true, false, true],
[false, true, false],
[true, true, false],
];
}

/**
* @dataProvider dataCreateBackupStatus
* @param bool $hasStatus
* @param bool $hasBackup
* @param bool $backupCreated
*/
public function testCreateBackupStatus(bool $hasStatus, bool $hasBackup, bool $backupCreated): void {
if ($hasStatus) {
$userStatus1 = new UserStatus();
$userStatus1->setUserId('user1');
$userStatus1->setStatus('online');
$userStatus1->setStatusTimestamp(5000);
$userStatus1->setIsUserDefined(true);
$userStatus1->setIsBackup(false);
$userStatus1->setCustomIcon('🚀');
$userStatus1->setCustomMessage('Current');
$userStatus1->setClearAt(50000);
$this->mapper->insert($userStatus1);
}

if ($hasBackup) {
$userStatus1 = new UserStatus();
$userStatus1->setUserId('_user1');
$userStatus1->setStatus('online');
$userStatus1->setStatusTimestamp(5000);
$userStatus1->setIsUserDefined(true);
$userStatus1->setIsBackup(true);
$userStatus1->setCustomIcon('🚀');
$userStatus1->setCustomMessage('Backup');
$userStatus1->setClearAt(50000);
$this->mapper->insert($userStatus1);
}

if ($hasStatus && $hasBackup) {
$this->expectException(Exception::class);
}

self::assertSame($backupCreated, $this->mapper->createBackupStatus('user1'));

if ($backupCreated) {
$user1Status = $this->mapper->findByUserId('user1', true);
$this->assertEquals('_user1', $user1Status->getUserId());
$this->assertEquals(true, $user1Status->getIsBackup());
$this->assertEquals('Current', $user1Status->getCustomMessage());
} else if ($hasBackup) {
$user1Status = $this->mapper->findByUserId('user1', true);
$this->assertEquals('_user1', $user1Status->getUserId());
$this->assertEquals(true, $user1Status->getIsBackup());
$this->assertEquals('Backup', $user1Status->getCustomMessage());
}
}

public function testRestoreBackupStatuses(): void {
$userStatus1 = new UserStatus();
$userStatus1->setUserId('_user1');
$userStatus1->setStatus('online');
$userStatus1->setStatusTimestamp(5000);
$userStatus1->setIsUserDefined(true);
$userStatus1->setIsBackup(true);
$userStatus1->setCustomIcon('🚀');
$userStatus1->setCustomMessage('Releasing');
$userStatus1->setClearAt(50000);
$userStatus1 = $this->mapper->insert($userStatus1);

$userStatus2 = new UserStatus();
$userStatus2->setUserId('_user2');
$userStatus2->setStatus('away');
$userStatus2->setStatusTimestamp(5000);
$userStatus2->setIsUserDefined(true);
$userStatus2->setIsBackup(true);
$userStatus2->setCustomIcon('💩');
$userStatus2->setCustomMessage('Do not disturb');
$userStatus2->setClearAt(50000);
$userStatus2 = $this->mapper->insert($userStatus2);

$userStatus3 = new UserStatus();
$userStatus3->setUserId('_user3');
$userStatus3->setStatus('away');
$userStatus3->setStatusTimestamp(5000);
$userStatus3->setIsUserDefined(true);
$userStatus3->setIsBackup(true);
$userStatus3->setCustomIcon('🏝️');
$userStatus3->setCustomMessage('Vacationing');
$userStatus3->setClearAt(50000);
$this->mapper->insert($userStatus3);

$this->mapper->restoreBackupStatuses([$userStatus1->getId(), $userStatus2->getId()]);

$user1Status = $this->mapper->findByUserId('user1', false);
$this->assertEquals('user1', $user1Status->getUserId());
$this->assertEquals(false, $user1Status->getIsBackup());
$this->assertEquals('Releasing', $user1Status->getCustomMessage());

$user2Status = $this->mapper->findByUserId('user2', false);
$this->assertEquals('user2', $user2Status->getUserId());
$this->assertEquals(false, $user2Status->getIsBackup());
$this->assertEquals('Do not disturb', $user2Status->getCustomMessage());

$user3Status = $this->mapper->findByUserId('user3', true);
$this->assertEquals('_user3', $user3Status->getUserId());
$this->assertEquals(true, $user3Status->getIsBackup());
$this->assertEquals('Vacationing', $user3Status->getCustomMessage());
}
}
Loading

0 comments on commit bf4acd5

Please sign in to comment.