diff --git a/resources/sql/autopatches/20150514.user.cache.2.sql b/resources/sql/autopatches/20150514.user.cache.2.sql new file mode 100644 index 0000000000..fc53324dc3 --- /dev/null +++ b/resources/sql/autopatches/20150514.user.cache.2.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_user.user + ADD availabilityCache VARCHAR(255) COLLATE {$COLLATE_TEXT}; + +ALTER TABLE {$NAMESPACE}_user.user + ADD availabilityCacheTTL INT UNSIGNED; diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index 6e80d19236..947098ff28 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -55,16 +55,7 @@ final class PhabricatorCalendarEventEditor case PhabricatorCalendarEventTransaction::TYPE_INVITE: $map = $xaction->getNewValue(); $phids = array_keys($map); - $invitees = array(); - - if ($map && !$this->getIsNewObject()) { - $invitees = id(new PhabricatorCalendarEventInviteeQuery()) - ->setViewer($this->getActor()) - ->withEventPHIDs(array($object->getPHID())) - ->withInviteePHIDs($phids) - ->execute(); - $invitees = mpull($invitees, null, 'getInviteePHID'); - } + $invitees = mpull($object->getInvitees(), null, 'getInviteePHID'); $old = array(); foreach ($phids as $phid) { @@ -193,6 +184,53 @@ final class PhabricatorCalendarEventEditor return $xactions; } + protected function applyFinalEffects($object, array $xactions) { + + // Clear the availability caches for users whose availability is affected + // by this edit. + + $invalidate_all = false; + $invalidate_phids = array(); + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case PhabricatorCalendarEventTransaction::TYPE_START_DATE: + case PhabricatorCalendarEventTransaction::TYPE_END_DATE: + case PhabricatorCalendarEventTransaction::TYPE_CANCEL: + case PhabricatorCalendarEventTransaction::TYPE_ALL_DAY: + // For these kinds of changes, we need to invalidate the availabilty + // caches for all attendees. + $invalidate_all = true; + break; + case PhabricatorCalendarEventTransaction::TYPE_INVITE: + foreach ($xaction->getNewValue() as $phid => $ignored) { + $invalidate_phids[$phid] = $phid; + } + break; + } + } + + $phids = mpull($object->getInvitees(), 'getInviteePHID'); + $phids = array_fuse($phids); + + if (!$invalidate_all) { + $phids = array_select_keys($phids, $invalidate_phids); + } + + if ($phids) { + $user = new PhabricatorUser(); + $conn_w = $user->establishConnection('w'); + queryfx( + $conn_w, + 'UPDATE %T SET availabilityCacheTTL = NULL + WHERE phid IN (%Ls) AND availabilityCacheTTL >= %d', + $user->getTableName(), + $phids, + $object->getDateFromForCache()); + } + + return $xactions; + } + protected function validateAllTransactions( PhabricatorLiskDAO $object, diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index c476911ff3..d68c207371 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -147,6 +147,19 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return parent::save(); } + /** + * Get the event start epoch for evaluating invitee availability. + * + * When assessing availability, we pretend events start earlier than they + * really. This allows us to mark users away for the entire duration of a + * series of back-to-back meetings, even if they don't strictly overlap. + * + * @return int Event start date for availability caches. + */ + public function getDateFromForCache() { + return ($this->getDateFrom() - phutil_units('15 minutes in seconds')); + } + private static $statusTexts = array( self::STATUS_AWAY => 'away', self::STATUS_SPORADIC => 'sporadic', diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index 2bcb62c2bb..1b4c7c5450 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -201,8 +201,16 @@ final class PhabricatorPeopleQuery } if ($this->needAvailability) { - // TODO: Add caching. - $rebuild = $users; + $rebuild = array(); + foreach ($users as $user) { + $cache = $user->getAvailabilityCache(); + if ($cache !== null) { + $user->attachAvailability($cache); + } else { + $rebuild[] = $user; + } + } + if ($rebuild) { $this->rebuildAvailabilityCache($rebuild); } @@ -405,11 +413,6 @@ final class PhabricatorPeopleQuery } } - // Margin between meetings: pretend meetings start earlier than they do - // so we mark you away for the entire time if you have a series of - // back-to-back meetings, even if they don't strictly overlap. - $margin = phutil_units('15 minutes in seconds'); - foreach ($rebuild as $phid => $user) { $events = idx($map, $phid, array()); @@ -419,7 +422,7 @@ final class PhabricatorPeopleQuery // because of an event, we check again for events after that one ends. while (true) { foreach ($events as $event) { - $from = ($event->getDateFrom() - $margin); + $from = $event->getDateFromForCache(); $to = $event->getDateTo(); if (($from <= $cursor) && ($to > $cursor)) { $cursor = $to; @@ -436,15 +439,16 @@ final class PhabricatorPeopleQuery ); $availability_ttl = $cursor; } else { - $availability = null; + $availability = array( + 'until' => null, + ); $availability_ttl = $max_range; } // Never TTL the cache to longer than the maximum range we examined. $availability_ttl = min($availability_ttl, $max_range); - // TODO: Write the cache. - + $user->writeAvailabilityCache($availability, $availability_ttl); $user->attachAvailability($availability); } } diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 89de29747b..0c7ef73b0e 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -27,6 +27,8 @@ final class PhabricatorUser protected $passwordHash; protected $profileImagePHID; protected $profileImageCache; + protected $availabilityCache; + protected $availabilityCacheTTL; protected $timezoneIdentifier = ''; protected $consoleEnabled = 0; @@ -146,6 +148,8 @@ final class PhabricatorUser 'accountSecret' => 'bytes64', 'isEnrolledInMultiFactor' => 'bool', 'profileImageCache' => 'text255?', + 'availabilityCache' => 'text255?', + 'availabilityCacheTTL' => 'uint32?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -166,6 +170,8 @@ final class PhabricatorUser ), self::CONFIG_NO_MUTATE => array( 'profileImageCache' => true, + 'availabilityCache' => true, + 'availabilityCacheTTL' => true, ), ) + parent::getConfiguration(); } @@ -721,7 +727,7 @@ EOBODY; /** * @task availability */ - public function attachAvailability($availability) { + public function attachAvailability(array $availability) { $this->availability = $availability; return $this; } @@ -762,6 +768,50 @@ EOBODY; } + /** + * Get cached availability, if present. + * + * @return wild|null Cache data, or null if no cache is available. + * @task availability + */ + public function getAvailabilityCache() { + $now = PhabricatorTime::getNow(); + if ($this->availabilityCacheTTL <= $now) { + return null; + } + + try { + return phutil_json_decode($this->availabilityCache); + } catch (Exception $ex) { + return null; + } + } + + + /** + * Write to the availability cache. + * + * @param wild Availability cache data. + * @param int|null Cache TTL. + * @return this + * @task availability + */ + public function writeAvailabilityCache(array $availability, $ttl) { + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + queryfx( + $this->establishConnection('w'), + 'UPDATE %T SET availabilityCache = %s, availabilityCacheTTL = %nd + WHERE id = %d', + $this->getTableName(), + json_encode($availability), + $ttl, + $this->getID()); + unset($unguarded); + + return $this; + } + + /* -( Profile Image Cache )------------------------------------------------ */