@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.

Split the GitHub import cursor into separate repository and issues event importers

Summary:
Ref T10538. The primary GitHub event activity stream does not report minor events (labels, milestones, etc).

GitHub has a second, similar activity stream which does report these events (the "Issues Events API").

Use two separate cursors: one consumes the primary stream; the second consumes the events stream.

One possible issue with this is that we may write events in a different order than they occurred, so GitHub shows "comment, label, close" but we show "comment, close, label" or similar. This is probably OK because the secondary API doesn't seem to have any very important events (e.g., it's probably fine if label changes are out-of-order), but we can conceivably put some buffer stage in between the two if it's an issue.

Test Plan: {F1164894}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10538

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

+361 -223
+5 -1
src/__phutil_library_map__.php
··· 1421 1421 'NuanceController' => 'applications/nuance/controller/NuanceController.php', 1422 1422 'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php', 1423 1423 'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php', 1424 + 'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php', 1425 + 'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php', 1424 1426 'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php', 1425 1427 'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php', 1426 1428 'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php', ··· 5677 5679 'NuanceController' => 'PhabricatorController', 5678 5680 'NuanceDAO' => 'PhabricatorLiskDAO', 5679 5681 'NuanceGitHubEventItemType' => 'NuanceItemType', 5680 - 'NuanceGitHubRepositoryImportCursor' => 'NuanceImportCursor', 5682 + 'NuanceGitHubImportCursor' => 'NuanceImportCursor', 5683 + 'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor', 5684 + 'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor', 5681 5685 'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition', 5682 5686 'NuanceImportCursor' => 'Phobject', 5683 5687 'NuanceImportCursorData' => array(
+258
src/applications/nuance/cursor/NuanceGitHubImportCursor.php
··· 1 + <?php 2 + 3 + abstract class NuanceGitHubImportCursor 4 + extends NuanceImportCursor { 5 + 6 + abstract protected function getGitHubAPIEndpointURI($user, $repository); 7 + abstract protected function newNuanceItemFromGitHubRecord(array $record); 8 + 9 + protected function getMaximumPage() { 10 + return 100; 11 + } 12 + 13 + protected function getPageSize() { 14 + return 100; 15 + } 16 + 17 + protected function getMinimumDelayBetweenPolls() { 18 + // Even if GitHub says we can, don't poll more than once every few seconds. 19 + // In particular, the Issue Events API does not advertise a poll interval 20 + // in a header. 21 + return 5; 22 + } 23 + 24 + final protected function shouldPullDataFromSource() { 25 + $now = PhabricatorTime::getNow(); 26 + 27 + // Respect GitHub's poll interval header. If we made a request recently, 28 + // don't make another one until we've waited long enough. 29 + $ttl = $this->getCursorProperty('github.poll.ttl'); 30 + if ($ttl && ($ttl >= $now)) { 31 + $this->logInfo( 32 + pht( 33 + 'Respecting "%s" or minimum poll delay: waiting for %s second(s) '. 34 + 'to poll GitHub.', 35 + 'X-Poll-Interval', 36 + new PhutilNumber(1 + ($ttl - $now)))); 37 + 38 + return false; 39 + } 40 + 41 + // Respect GitHub's API rate limiting. If we've exceeded the rate limit, 42 + // wait until it resets to try again. 43 + $limit = $this->getCursorProperty('github.limit.ttl'); 44 + if ($limit && ($limit >= $now)) { 45 + $this->logInfo( 46 + pht( 47 + 'Respecting "%s": waiting for %s second(s) to poll GitHub.', 48 + 'X-RateLimit-Reset', 49 + new PhutilNumber(1 + ($limit - $now)))); 50 + return false; 51 + } 52 + 53 + return true; 54 + } 55 + 56 + final protected function pullDataFromSource() { 57 + $viewer = $this->getViewer(); 58 + $now = PhabricatorTime::getNow(); 59 + 60 + $source = $this->getSource(); 61 + 62 + $user = $source->getSourceProperty('github.user'); 63 + $repository = $source->getSourceProperty('github.repository'); 64 + $api_token = $source->getSourceProperty('github.token'); 65 + 66 + // This API only supports fetching 10 pages of 30 events each, for a total 67 + // of 300 events. 68 + $etag = null; 69 + $new_items = array(); 70 + $hit_known_items = false; 71 + 72 + $max_page = $this->getMaximumPage(); 73 + $page_size = $this->getPageSize(); 74 + 75 + for ($page = 1; $page <= $max_page; $page++) { 76 + $uri = $this->getGitHubAPIEndpointURI($user, $repository); 77 + 78 + $data = array( 79 + 'page' => $page, 80 + 'per_page' => $page_size, 81 + ); 82 + 83 + $future = id(new PhutilGitHubFuture()) 84 + ->setAccessToken($api_token) 85 + ->setRawGitHubQuery($uri, $data); 86 + 87 + if ($page == 1) { 88 + $cursor_etag = $this->getCursorProperty('github.poll.etag'); 89 + if ($cursor_etag) { 90 + $future->addHeader('If-None-Match', $cursor_etag); 91 + } 92 + } 93 + 94 + $this->logInfo( 95 + pht( 96 + 'Polling GitHub Repository API endpoint "%s".', 97 + $uri)); 98 + $response = $future->resolve(); 99 + 100 + // Do this first: if we hit the rate limit, we get a response but the 101 + // body isn't valid. 102 + $this->updateRateLimits($response); 103 + 104 + if ($response->getStatus()->getStatusCode() == 304) { 105 + $this->logInfo( 106 + pht( 107 + 'Received a 304 Not Modified from GitHub, no new events.')); 108 + } 109 + 110 + // This means we hit a rate limit or a "Not Modified" because of the 111 + // "ETag" header. In either case, we should bail out. 112 + if ($response->getStatus()->isError()) { 113 + $this->updatePolling($response, $now, false); 114 + $this->getCursorData()->save(); 115 + return false; 116 + } 117 + 118 + if ($page == 1) { 119 + $etag = $response->getHeaderValue('ETag'); 120 + } 121 + 122 + $records = $response->getBody(); 123 + foreach ($records as $record) { 124 + $item = $this->newNuanceItemFromGitHubRecord($record); 125 + $item_key = $item->getItemKey(); 126 + 127 + $this->logInfo( 128 + pht( 129 + 'Fetched event "%s".', 130 + $item_key)); 131 + 132 + $new_items[$item->getItemKey()] = $item; 133 + } 134 + 135 + if ($new_items) { 136 + $existing = id(new NuanceItemQuery()) 137 + ->setViewer($viewer) 138 + ->withSourcePHIDs(array($source->getPHID())) 139 + ->withItemKeys(array_keys($new_items)) 140 + ->execute(); 141 + $existing = mpull($existing, null, 'getItemKey'); 142 + foreach ($new_items as $key => $new_item) { 143 + if (isset($existing[$key])) { 144 + unset($new_items[$key]); 145 + $hit_known_items = true; 146 + 147 + $this->logInfo( 148 + pht( 149 + 'Event "%s" is previously known.', 150 + $key)); 151 + } 152 + } 153 + } 154 + 155 + if ($hit_known_items) { 156 + break; 157 + } 158 + 159 + if (count($records) < $page_size) { 160 + break; 161 + } 162 + } 163 + 164 + // TODO: When we go through the whole queue without hitting anything we 165 + // have seen before, we should record some sort of global event so we 166 + // can tell the user when the bridging started or was interrupted? 167 + if (!$hit_known_items) { 168 + $already_polled = $this->getCursorProperty('github.polled'); 169 + if ($already_polled) { 170 + // TODO: This is bad: we missed some items, maybe because too much 171 + // stuff happened too fast or the daemons were broken for a long 172 + // time. 173 + } else { 174 + // TODO: This is OK, we're doing the initial import. 175 + } 176 + } 177 + 178 + if ($etag !== null) { 179 + $this->updateETag($etag); 180 + } 181 + 182 + $this->updatePolling($response, $now, true); 183 + 184 + // Reverse the new items so we insert them in chronological order. 185 + $new_items = array_reverse($new_items); 186 + 187 + $source->openTransaction(); 188 + foreach ($new_items as $new_item) { 189 + $new_item->save(); 190 + } 191 + $this->getCursorData()->save(); 192 + $source->saveTransaction(); 193 + 194 + foreach ($new_items as $new_item) { 195 + $new_item->scheduleUpdate(); 196 + } 197 + 198 + return false; 199 + } 200 + 201 + private function updateRateLimits(PhutilGitHubResponse $response) { 202 + $remaining = $response->getHeaderValue('X-RateLimit-Remaining'); 203 + $limit_reset = $response->getHeaderValue('X-RateLimit-Reset'); 204 + $now = PhabricatorTime::getNow(); 205 + 206 + $limit_ttl = null; 207 + if (strlen($remaining)) { 208 + $remaining = (int)$remaining; 209 + if (!$remaining) { 210 + $limit_ttl = (int)$limit_reset; 211 + } 212 + } 213 + 214 + $this->setCursorProperty('github.limit.ttl', $limit_ttl); 215 + 216 + $this->logInfo( 217 + pht( 218 + 'This key has %s remaining API request(s), '. 219 + 'limit resets in %s second(s).', 220 + new PhutilNumber($remaining), 221 + new PhutilNumber($limit_reset - $now))); 222 + } 223 + 224 + private function updateETag($etag) { 225 + 226 + $this->setCursorProperty('github.poll.etag', $etag); 227 + 228 + $this->logInfo( 229 + pht( 230 + 'ETag for this request was "%s".', 231 + $etag)); 232 + } 233 + 234 + private function updatePolling( 235 + PhutilGitHubResponse $response, 236 + $start, 237 + $success) { 238 + 239 + if ($success) { 240 + $this->setCursorProperty('github.polled', true); 241 + } 242 + 243 + $poll_interval = (int)$response->getHeaderValue('X-Poll-Interval'); 244 + $poll_interval = max($this->getMinimumDelayBetweenPolls(), $poll_interval); 245 + 246 + $poll_ttl = $start + $poll_interval; 247 + $this->setCursorProperty('github.poll.ttl', $poll_ttl); 248 + 249 + $now = PhabricatorTime::getNow(); 250 + 251 + $this->logInfo( 252 + pht( 253 + 'Set API poll TTL to +%s second(s) (%s second(s) from now).', 254 + new PhutilNumber($poll_interval), 255 + new PhutilNumber($poll_ttl - $now))); 256 + } 257 + 258 + }
+30
src/applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php
··· 1 + <?php 2 + 3 + final class NuanceGitHubIssuesImportCursor 4 + extends NuanceGitHubImportCursor { 5 + 6 + const CURSORTYPE = 'github.issues'; 7 + 8 + protected function getGitHubAPIEndpointURI($user, $repository) { 9 + return "/repos/{$user}/{$repository}/issues/events"; 10 + } 11 + 12 + protected function newNuanceItemFromGitHubRecord(array $record) { 13 + $source = $this->getSource(); 14 + 15 + $id = $record['id']; 16 + $item_key = "github.issueevent.{$id}"; 17 + 18 + $container_key = null; 19 + 20 + return NuanceItem::initializeNewItem() 21 + ->setStatus(NuanceItem::STATUS_IMPORTING) 22 + ->setSourcePHID($source->getPHID()) 23 + ->setItemType(NuanceGitHubEventItemType::ITEMTYPE) 24 + ->setItemKey($item_key) 25 + ->setItemContainerKey($container_key) 26 + ->setItemProperty('api.type', 'issue') 27 + ->setItemProperty('api.raw', $record); 28 + } 29 + 30 + }
+9 -221
src/applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php
··· 1 1 <?php 2 2 3 3 final class NuanceGitHubRepositoryImportCursor 4 - extends NuanceImportCursor { 4 + extends NuanceGitHubImportCursor { 5 5 6 6 const CURSORTYPE = 'github.repository'; 7 7 8 - protected function shouldPullDataFromSource() { 9 - $now = PhabricatorTime::getNow(); 10 - 11 - // Respect GitHub's poll interval header. If we made a request recently, 12 - // don't make another one until we've waited long enough. 13 - $ttl = $this->getCursorProperty('github.poll.ttl'); 14 - if ($ttl && ($ttl >= $now)) { 15 - $this->logInfo( 16 - pht( 17 - 'Respecting "%s": waiting for %s second(s) to poll GitHub.', 18 - 'X-Poll-Interval', 19 - new PhutilNumber(1 + ($ttl - $now)))); 20 - 21 - return false; 22 - } 23 - 24 - // Respect GitHub's API rate limiting. If we've exceeded the rate limit, 25 - // wait until it resets to try again. 26 - $limit = $this->getCursorProperty('github.limit.ttl'); 27 - if ($limit && ($limit >= $now)) { 28 - $this->logInfo( 29 - pht( 30 - 'Respecting "%s": waiting for %s second(s) to poll GitHub.', 31 - 'X-RateLimit-Reset', 32 - new PhutilNumber(1 + ($limit - $now)))); 33 - return false; 34 - } 35 - 36 - return true; 37 - } 38 - 39 - protected function pullDataFromSource() { 40 - $viewer = $this->getViewer(); 41 - $now = PhabricatorTime::getNow(); 42 - 43 - $source = $this->getSource(); 44 - 45 - $user = $source->getSourceProperty('github.user'); 46 - $repository = $source->getSourceProperty('github.repository'); 47 - $api_token = $source->getSourceProperty('github.token'); 48 - 49 - // This API only supports fetching 10 pages of 30 events each, for a total 50 - // of 300 events. 51 - $etag = null; 52 - $new_items = array(); 53 - $hit_known_items = false; 54 - for ($page = 1; $page <= 10; $page++) { 55 - $uri = "/repos/{$user}/{$repository}/events"; 56 - $data = array( 57 - 'page' => $page, 58 - ); 59 - 60 - $future = id(new PhutilGitHubFuture()) 61 - ->setAccessToken($api_token) 62 - ->setRawGitHubQuery($uri, $data); 63 - 64 - if ($page == 1) { 65 - $cursor_etag = $this->getCursorProperty('github.poll.etag'); 66 - if ($cursor_etag) { 67 - $future->addHeader('If-None-Match', $cursor_etag); 68 - } 69 - } 70 - 71 - $this->logInfo( 72 - pht( 73 - 'Polling GitHub Repository API endpoint "%s".', 74 - $uri)); 75 - $response = $future->resolve(); 76 - 77 - // Do this first: if we hit the rate limit, we get a response but the 78 - // body isn't valid. 79 - $this->updateRateLimits($response); 80 - 81 - if ($response->getStatus()->getStatusCode() == 304) { 82 - $this->logInfo( 83 - pht( 84 - 'Received a 304 Not Modified from GitHub, no new events.')); 85 - } 86 - 87 - // This means we hit a rate limit or a "Not Modified" because of the 88 - // "ETag" header. In either case, we should bail out. 89 - if ($response->getStatus()->isError()) { 90 - $this->updatePolling($response, $now, false); 91 - $this->getCursorData()->save(); 92 - return false; 93 - } 94 - 95 - if ($page == 1) { 96 - $etag = $response->getHeaderValue('ETag'); 97 - } 98 - 99 - $records = $response->getBody(); 100 - foreach ($records as $record) { 101 - $item = $this->newNuanceItemFromGitHubEvent($record); 102 - $item_key = $item->getItemKey(); 103 - 104 - $this->logInfo( 105 - pht( 106 - 'Fetched event "%s".', 107 - $item_key)); 108 - 109 - $new_items[$item->getItemKey()] = $item; 110 - } 111 - 112 - if ($new_items) { 113 - $existing = id(new NuanceItemQuery()) 114 - ->setViewer($viewer) 115 - ->withSourcePHIDs(array($source->getPHID())) 116 - ->withItemKeys(array_keys($new_items)) 117 - ->execute(); 118 - $existing = mpull($existing, null, 'getItemKey'); 119 - foreach ($new_items as $key => $new_item) { 120 - if (isset($existing[$key])) { 121 - unset($new_items[$key]); 122 - $hit_known_items = true; 123 - 124 - $this->logInfo( 125 - pht( 126 - 'Event "%s" is previously known.', 127 - $key)); 128 - } 129 - } 130 - } 131 - 132 - if ($hit_known_items) { 133 - break; 134 - } 135 - 136 - if (count($records) < 30) { 137 - break; 138 - } 139 - } 140 - 141 - // TODO: When we go through the whole queue without hitting anything we 142 - // have seen before, we should record some sort of global event so we 143 - // can tell the user when the bridging started or was interrupted? 144 - if (!$hit_known_items) { 145 - $already_polled = $this->getCursorProperty('github.polled'); 146 - if ($already_polled) { 147 - // TODO: This is bad: we missed some items, maybe because too much 148 - // stuff happened too fast or the daemons were broken for a long 149 - // time. 150 - } else { 151 - // TODO: This is OK, we're doing the initial import. 152 - } 153 - } 154 - 155 - if ($etag !== null) { 156 - $this->updateETag($etag); 157 - } 158 - 159 - $this->updatePolling($response, $now, true); 160 - 161 - // Reverse the new items so we insert them in chronological order. 162 - $new_items = array_reverse($new_items); 163 - 164 - $source->openTransaction(); 165 - foreach ($new_items as $new_item) { 166 - $new_item->save(); 167 - } 168 - $this->getCursorData()->save(); 169 - $source->saveTransaction(); 170 - 171 - foreach ($new_items as $new_item) { 172 - $new_item->scheduleUpdate(); 173 - } 174 - 175 - return false; 8 + protected function getGitHubAPIEndpointURI($user, $repository) { 9 + return "/repos/{$user}/{$repository}/events"; 176 10 } 177 11 178 - private function updateRateLimits(PhutilGitHubResponse $response) { 179 - $remaining = $response->getHeaderValue('X-RateLimit-Remaining'); 180 - $limit_reset = $response->getHeaderValue('X-RateLimit-Reset'); 181 - $now = PhabricatorTime::getNow(); 182 - 183 - $limit_ttl = null; 184 - if (strlen($remaining)) { 185 - $remaining = (int)$remaining; 186 - if (!$remaining) { 187 - $limit_ttl = (int)$limit_reset; 188 - } 189 - } 190 - 191 - $this->setCursorProperty('github.limit.ttl', $limit_ttl); 192 - 193 - $this->logInfo( 194 - pht( 195 - 'This key has %s remaining API request(s), '. 196 - 'limit resets in %s second(s).', 197 - new PhutilNumber($remaining), 198 - new PhutilNumber($limit_reset - $now))); 12 + protected function getMaximumPage() { 13 + return 10; 199 14 } 200 15 201 - private function updateETag($etag) { 202 - 203 - $this->setCursorProperty('github.poll.etag', $etag); 204 - 205 - $this->logInfo( 206 - pht( 207 - 'ETag for this request was "%s".', 208 - $etag)); 16 + protected function getPageSize() { 17 + return 30; 209 18 } 210 19 211 - private function updatePolling( 212 - PhutilGitHubResponse $response, 213 - $start, 214 - $success) { 215 - 216 - if ($success) { 217 - $this->setCursorProperty('github.polled', true); 218 - } 219 - 220 - $poll_interval = (int)$response->getHeaderValue('X-Poll-Interval'); 221 - $poll_ttl = $start + $poll_interval; 222 - $this->setCursorProperty('github.poll.ttl', $poll_ttl); 223 - 224 - $now = PhabricatorTime::getNow(); 225 - 226 - $this->logInfo( 227 - pht( 228 - 'Set API poll TTL to +%s second(s) (%s second(s) from now).', 229 - new PhutilNumber($poll_interval), 230 - new PhutilNumber($poll_ttl - $now))); 231 - } 232 - 233 - private function newNuanceItemFromGitHubEvent(array $record) { 20 + protected function newNuanceItemFromGitHubRecord(array $record) { 234 21 $source = $this->getSource(); 235 22 236 23 $id = $record['id']; ··· 255 42 ->setItemType(NuanceGitHubEventItemType::ITEMTYPE) 256 43 ->setItemKey($item_key) 257 44 ->setItemContainerKey($container_key) 45 + ->setItemProperty('api.type', 'repository') 258 46 ->setItemProperty('api.raw', $record); 259 47 } 260 48
+21
src/applications/nuance/item/NuanceGitHubEventItemType.php
··· 14 14 } 15 15 16 16 public function getItemDisplayName(NuanceItem $item) { 17 + $api_type = $item->getItemProperty('api.type'); 18 + switch ($api_type) { 19 + case 'issue': 20 + return $this->getGitHubIssueAPIEventDisplayName($item); 21 + case 'repository': 22 + return $this->getGitHubRepositoryAPIEventDisplayName($item); 23 + default: 24 + return pht('GitHub Event (Unknown API Type "%s")', $api_type); 25 + } 26 + } 27 + 28 + private function getGitHubIssueAPIEventDisplayName(NuanceItem $item) { 29 + $raw = $item->getItemProperty('api.raw', array()); 30 + 31 + $action = idxv($raw, array('event')); 32 + $number = idxv($raw, array('issue', 'number')); 33 + 34 + return pht('GitHub Issue #%d (%s)', $number, $action); 35 + } 36 + 37 + private function getGitHubRepositoryAPIEventDisplayName(NuanceItem $item) { 17 38 $raw = $item->getItemProperty('api.raw', array()); 18 39 19 40 $repo = idxv($raw, array('repo', 'name'), pht('<unknown/unknown>'));
+35
src/applications/nuance/management/NuanceManagementImportWorkflow.php
··· 15 15 'param' => 'source', 16 16 'help' => pht('Choose which source to import.'), 17 17 ), 18 + array( 19 + 'name' => 'cursor', 20 + 'param' => 'cursor', 21 + 'help' => pht('Import only a particular cursor.'), 22 + ), 18 23 )); 19 24 } 20 25 ··· 38 43 pht( 39 44 'This source ("%s") does not have any import cursors.', 40 45 $source->getName())); 46 + } 47 + 48 + $select = $args->getArg('cursor'); 49 + if (strlen($select)) { 50 + if (empty($cursors[$select])) { 51 + throw new PhutilArgumentUsageException( 52 + pht( 53 + 'This source ("%s") does not have a "%s" cursor. Available '. 54 + 'cursors: %s.', 55 + $source->getName(), 56 + $select, 57 + implode(', ', array_keys($cursors)))); 58 + } else { 59 + echo tsprintf( 60 + "%s\n", 61 + pht( 62 + 'Importing cursor "%s" only.', 63 + $select)); 64 + $cursors = array_select_keys($cursors, array($select)); 65 + } 66 + } else { 67 + echo tsprintf( 68 + "%s\n", 69 + pht( 70 + 'Importing all cursors: %s.', 71 + implode(', ', array_keys($cursors)))); 72 + 73 + echo tsprintf( 74 + "%s\n", 75 + pht('(Use --cursor to import only a particular cursor.)')); 41 76 } 42 77 43 78 foreach ($cursors as $cursor) {
+2
src/applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php
··· 23 23 return array( 24 24 id(new NuanceGitHubRepositoryImportCursor()) 25 25 ->setCursorKey('events.repository'), 26 + id(new NuanceGitHubIssuesImportCursor()) 27 + ->setCursorKey('events.issues'), 26 28 ); 27 29 } 28 30
+1 -1
src/applications/nuance/source/NuanceSourceDefinition.php
··· 114 114 ->setCursorData($cursor_data); 115 115 } 116 116 117 - return $cursors; 117 + return $map; 118 118 } 119 119 120 120 protected function newImportCursors() {