@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Cache user notification and message counts

Summary:
Ref T4103. Ref T10078. This puts a user cache in front of notification and message counts.

This reduces the number of queries issued on every page by 4 (2x building the menu, 2x building Quicksand data).

Also fixes some minor issues:

- Daemons could choke on sending mail in the user's translation.
- No-op object updates could fail in the daemons.
- Questionable data access pattern in the file query coming out of the profile file cache.

Test Plan:
- Sent myself notifications. Saw count go up.
- Cleared them by visiting objects and clearing all notifications. Saw count go down.
- Sent myself messages. Saw count go up.
- Cleared them by visiting threads. Saw count go down.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T4103, T10078

Differential Revision: https://secure.phabricator.com/D16041

+174 -39
+4
src/__phutil_library_map__.php
··· 3640 3640 'PhabricatorUserIconField' => 'applications/people/customfield/PhabricatorUserIconField.php', 3641 3641 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', 3642 3642 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 3643 + 'PhabricatorUserMessageCountCacheType' => 'applications/people/cache/PhabricatorUserMessageCountCacheType.php', 3644 + 'PhabricatorUserNotificationCountCacheType' => 'applications/people/cache/PhabricatorUserNotificationCountCacheType.php', 3643 3645 'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php', 3644 3646 'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php', 3645 3647 'PhabricatorUserPreferencesCacheType' => 'applications/people/cache/PhabricatorUserPreferencesCacheType.php', ··· 8451 8453 'PhabricatorPolicyInterface', 8452 8454 ), 8453 8455 'PhabricatorUserLogView' => 'AphrontView', 8456 + 'PhabricatorUserMessageCountCacheType' => 'PhabricatorUserCacheType', 8457 + 'PhabricatorUserNotificationCountCacheType' => 'PhabricatorUserCacheType', 8454 8458 'PhabricatorUserPHIDResolver' => 'PhabricatorPHIDResolver', 8455 8459 'PhabricatorUserPreferences' => array( 8456 8460 'PhabricatorUserDAO',
+11 -13
src/applications/aphlict/query/AphlictDropdownDataQuery.php
··· 46 46 $is_c_installed = PhabricatorApplication::isClassInstalledForViewer( 47 47 $conpherence_app, 48 48 $viewer); 49 - $raw_message_count_number = null; 50 - $message_count_number = null; 51 49 if ($is_c_installed) { 52 - $unread_status = ConpherenceParticipationStatus::BEHIND; 53 - $unread = id(new ConpherenceParticipantCountQuery()) 54 - ->withParticipantPHIDs(array($viewer->getPHID())) 55 - ->withParticipationStatus($unread_status) 56 - ->execute(); 57 - $raw_message_count_number = idx($unread, $viewer->getPHID(), 0); 50 + $raw_message_count_number = $viewer->getUnreadMessageCount(); 58 51 $message_count_number = $this->formatNumber($raw_message_count_number); 52 + } else { 53 + $raw_message_count_number = null; 54 + $message_count_number = null; 59 55 } 56 + 57 + 60 58 $conpherence_data = array( 61 59 'isInstalled' => $is_c_installed, 62 60 'countType' => 'messages', ··· 69 67 $is_n_installed = PhabricatorApplication::isClassInstalledForViewer( 70 68 $notification_app, 71 69 $viewer); 72 - $notification_count_number = null; 73 - $raw_notification_count_number = null; 74 70 if ($is_n_installed) { 75 - $raw_notification_count_number = 76 - id(new PhabricatorFeedStoryNotification()) 77 - ->countUnread($viewer); 71 + $raw_notification_count_number = $viewer->getUnreadNotificationCount(); 78 72 $notification_count_number = $this->formatNumber( 79 73 $raw_notification_count_number); 74 + } else { 75 + $notification_count_number = null; 76 + $raw_notification_count_number = null; 80 77 } 78 + 81 79 $notification_data = array( 82 80 'isInstalled' => $is_n_installed, 83 81 'countType' => 'notifications',
+6 -3
src/applications/conpherence/controller/ConpherenceViewController.php
··· 68 68 $latest_transaction = head($transactions); 69 69 $participant = $conpherence->getParticipantIfExists($user->getPHID()); 70 70 if ($participant) { 71 - $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); 72 - $participant->markUpToDate($conpherence, $latest_transaction); 73 - unset($write_guard); 71 + if (!$participant->isUpToDate($conpherence)) { 72 + $write_guard = AphrontWriteGuard::beginScopedUnguardedWrites(); 73 + $participant->markUpToDate($conpherence, $latest_transaction); 74 + $user->clearCacheData(PhabricatorUserMessageCountCacheType::KEY_COUNT); 75 + unset($write_guard); 76 + } 74 77 } 75 78 76 79 $data = ConpherenceTransactionRenderer::renderTransactions(
+4
src/applications/conpherence/editor/ConpherenceEditor.php
··· 422 422 $participant->save(); 423 423 } 424 424 425 + PhabricatorUserCache::clearCaches( 426 + PhabricatorUserMessageCountCacheType::KEY_COUNT, 427 + array_keys($participants)); 428 + 425 429 if ($xactions) { 426 430 $data = array( 427 431 'type' => 'message',
+6 -1
src/applications/conpherence/storage/ConpherenceParticipant.php
··· 47 47 $this->setBehindTransactionPHID($xaction->getPHID()); 48 48 $this->setSeenMessageCount($conpherence->getMessageCount()); 49 49 $this->save(); 50 + 51 + PhabricatorUserCache::clearCache( 52 + PhabricatorUserMessageCountCacheType::KEY_COUNT, 53 + $this->getParticipantPHID()); 50 54 } 55 + 51 56 return $this; 52 57 } 53 58 54 - private function isUpToDate(ConpherenceThread $conpherence) { 59 + public function isUpToDate(ConpherenceThread $conpherence) { 55 60 return 56 61 ($this->getSeenMessageCount() == $conpherence->getMessageCount()) 57 62 &&
+6 -1
src/applications/feed/PhabricatorFeedStoryPublisher.php
··· 159 159 160 160 $will_receive_mail = array_fill_keys($this->mailRecipientPHIDs, true); 161 161 162 - foreach (array_unique($subscribed_phids) as $user_phid) { 162 + $user_phids = array_unique($subscribed_phids); 163 + foreach ($user_phids as $user_phid) { 163 164 if (isset($will_receive_mail[$user_phid])) { 164 165 $mark_read = 1; 165 166 } else { ··· 184 185 $notif->getTableName(), 185 186 implode(', ', $sql)); 186 187 } 188 + 189 + PhabricatorUserCache::clearCaches( 190 + PhabricatorUserNotificationCountCacheType::KEY_COUNT, 191 + $user_phids); 187 192 } 188 193 189 194 private function sendNotification($chrono_key, array $subscribed_phids) {
+10
src/applications/files/query/PhabricatorFileQuery.php
··· 134 134 return $files; 135 135 } 136 136 137 + $viewer = $this->getViewer(); 138 + $is_omnipotent = $viewer->isOmnipotent(); 139 + 137 140 // We need to load attached objects to perform policy checks for files. 138 141 // First, load the edges. 139 142 ··· 153 156 // If this is a profile image, don't bother loading related files. 154 157 // It will always be visible, and we can get into trouble if we try 155 158 // to load objects and end up stuck in a cycle. See T8478. 159 + continue; 160 + } 161 + 162 + if ($is_omnipotent) { 163 + // If the viewer is omnipotent, we don't need to load the associated 164 + // objects either since they can certainly see the object. Skipping 165 + // this can improve performance and prevent cycles. 156 166 continue; 157 167 } 158 168
+4
src/applications/notification/controller/PhabricatorNotificationClearController.php
··· 18 18 $viewer->getPHID(), 19 19 $chrono_key); 20 20 21 + PhabricatorUserCache::clearCache( 22 + PhabricatorUserNotificationCountCacheType::KEY_COUNT, 23 + $viewer->getPHID()); 24 + 21 25 return id(new AphrontReloadResponse()) 22 26 ->setURI('/notification/'); 23 27 }
+1 -2
src/applications/notification/controller/PhabricatorNotificationPanelController.php
··· 71 71 $content, 72 72 $connection_ui); 73 73 74 - $unread_count = id(new PhabricatorFeedStoryNotification()) 75 - ->countUnread($viewer); 74 + $unread_count = $viewer->getUnreadNotificationCount(); 76 75 77 76 $json = array( 78 77 'content' => $content,
+3 -13
src/applications/notification/storage/PhabricatorFeedStoryNotification.php
··· 60 60 $object_phid); 61 61 62 62 unset($unguarded); 63 - } 64 63 65 - public function countUnread(PhabricatorUser $user) { 66 - $conn = $this->establishConnection('r'); 67 - 68 - $data = queryfx_one( 69 - $conn, 70 - 'SELECT COUNT(*) as count 71 - FROM %T 72 - WHERE userPHID = %s AND hasViewed = 0', 73 - $this->getTableName(), 74 - $user->getPHID()); 75 - 76 - return $data['count']; 64 + $count_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT; 65 + PhabricatorUserCache::clearCache($count_key, $user->getPHID()); 66 + $user->clearCacheData($count_key); 77 67 } 78 68 79 69 }
+45
src/applications/people/cache/PhabricatorUserMessageCountCacheType.php
··· 1 + <?php 2 + 3 + final class PhabricatorUserMessageCountCacheType 4 + extends PhabricatorUserCacheType { 5 + 6 + const CACHETYPE = 'message.count'; 7 + 8 + const KEY_COUNT = 'user.message.count.v1'; 9 + 10 + public function getAutoloadKeys() { 11 + return array( 12 + self::KEY_COUNT, 13 + ); 14 + } 15 + 16 + public function canManageKey($key) { 17 + return ($key === self::KEY_COUNT); 18 + } 19 + 20 + public function getValueFromStorage($value) { 21 + return (int)$value; 22 + } 23 + 24 + public function getValueForStorage($value) { 25 + return $value; 26 + } 27 + 28 + public function newValueForUsers($key, array $users) { 29 + if (!$users) { 30 + return array(); 31 + } 32 + 33 + $user_phids = mpull($users, 'getPHID'); 34 + 35 + $unread_status = ConpherenceParticipationStatus::BEHIND; 36 + $unread = id(new ConpherenceParticipantCountQuery()) 37 + ->withParticipantPHIDs($user_phids) 38 + ->withParticipationStatus($unread_status) 39 + ->execute(); 40 + 41 + $empty = array_fill_keys($user_phids, 0); 42 + return $unread + $empty; 43 + } 44 + 45 + }
+50
src/applications/people/cache/PhabricatorUserNotificationCountCacheType.php
··· 1 + <?php 2 + 3 + final class PhabricatorUserNotificationCountCacheType 4 + extends PhabricatorUserCacheType { 5 + 6 + const CACHETYPE = 'notification.count'; 7 + 8 + const KEY_COUNT = 'user.notification.count.v1'; 9 + 10 + public function getAutoloadKeys() { 11 + return array( 12 + self::KEY_COUNT, 13 + ); 14 + } 15 + 16 + public function canManageKey($key) { 17 + return ($key === self::KEY_COUNT); 18 + } 19 + 20 + public function getValueFromStorage($value) { 21 + return (int)$value; 22 + } 23 + 24 + public function getValueForStorage($value) { 25 + return $value; 26 + } 27 + 28 + public function newValueForUsers($key, array $users) { 29 + if (!$users) { 30 + return array(); 31 + } 32 + 33 + $user_phids = mpull($users, 'getPHID'); 34 + 35 + $table = new PhabricatorFeedStoryNotification(); 36 + $conn_r = $table->establishConnection('r'); 37 + 38 + $rows = queryfx_all( 39 + $conn_r, 40 + 'SELECT userPHID, COUNT(*) N FROM %T 41 + WHERE userPHID IN (%Ls) AND hasViewed = 0 42 + GROUP BY userPHID', 43 + $table->getTableName(), 44 + $user_phids); 45 + 46 + $empty = array_fill_keys($user_phids, 0); 47 + return ipull($rows, 'N', 'userPHID') + $empty; 48 + } 49 + 50 + }
+10
src/applications/people/storage/PhabricatorUser.php
··· 792 792 return $this->requireCacheData($uri_key); 793 793 } 794 794 795 + public function getUnreadNotificationCount() { 796 + $notification_key = PhabricatorUserNotificationCountCacheType::KEY_COUNT; 797 + return $this->requireCacheData($notification_key); 798 + } 799 + 800 + public function getUnreadMessageCount() { 801 + $message_key = PhabricatorUserMessageCountCacheType::KEY_COUNT; 802 + return $this->requireCacheData($message_key); 803 + } 804 + 795 805 public function getFullName() { 796 806 if (strlen($this->getRealName())) { 797 807 return $this->getUsername().' ('.$this->getRealName().')';
+10 -3
src/applications/people/storage/PhabricatorUserCache.php
··· 97 97 } 98 98 99 99 public static function clearCache($key, $user_phid) { 100 + return self::clearCaches($key, array($user_phid)); 101 + } 102 + 103 + public static function clearCaches($key, array $user_phids) { 100 104 if (PhabricatorEnv::isReadOnly()) { 101 105 return; 102 106 } 103 107 108 + if (!$user_phids) { 109 + return; 110 + } 111 + 104 112 $table = new self(); 105 113 $conn_w = $table->establishConnection('w'); 106 114 ··· 108 116 109 117 queryfx( 110 118 $conn_w, 111 - 'DELETE FROM %T WHERE cacheIndex = %s AND userPHID = %s', 119 + 'DELETE FROM %T WHERE cacheIndex = %s AND userPHID IN (%Ls)', 112 120 $table->getTableName(), 113 121 PhabricatorHash::digestForIndex($key), 114 - $user_phid); 122 + $user_phids); 115 123 116 124 unset($unguarded); 117 125 } 118 - 119 126 120 127 public static function clearCacheForAllUsers($key) { 121 128 if (PhabricatorEnv::isReadOnly()) {
-1
src/applications/settings/editor/PhabricatorUserPreferencesEditor.php
··· 162 162 PhabricatorUserPreferencesCacheType::KEY_PREFERENCES); 163 163 } 164 164 165 - 166 165 return $xactions; 167 166 } 168 167
+4 -2
src/applications/transactions/worker/PhabricatorApplicationTransactionPublishWorker.php
··· 84 84 85 85 $xaction_phids = idx($data, 'xactionPHIDs'); 86 86 if (!$xaction_phids) { 87 - throw new PhabricatorWorkerPermanentFailureException( 88 - pht('Task has no transaction PHIDs!')); 87 + // It's okay if we don't have any transactions. This can happen when 88 + // creating objects or performing no-op updates. We will still apply 89 + // meaningful side effects like updating search engine indexes. 90 + return array(); 89 91 } 90 92 91 93 $viewer = PhabricatorUser::getOmnipotentUser();