@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<?php
2
3abstract class DrydockWorker extends PhabricatorWorker {
4
5 protected function getViewer() {
6 return PhabricatorUser::getOmnipotentUser();
7 }
8
9 protected function loadLease($lease_phid) {
10 $viewer = $this->getViewer();
11
12 $lease = id(new DrydockLeaseQuery())
13 ->setViewer($viewer)
14 ->withPHIDs(array($lease_phid))
15 ->executeOne();
16 if (!$lease) {
17 throw new PhabricatorWorkerPermanentFailureException(
18 pht('No such lease "%s"!', $lease_phid));
19 }
20
21 return $lease;
22 }
23
24 protected function loadResource($resource_phid) {
25 $viewer = $this->getViewer();
26
27 $resource = id(new DrydockResourceQuery())
28 ->setViewer($viewer)
29 ->withPHIDs(array($resource_phid))
30 ->executeOne();
31 if (!$resource) {
32 throw new PhabricatorWorkerPermanentFailureException(
33 pht('No such resource "%s"!', $resource_phid));
34 }
35
36 return $resource;
37 }
38
39 protected function loadOperation($operation_phid) {
40 $viewer = $this->getViewer();
41
42 $operation = id(new DrydockRepositoryOperationQuery())
43 ->setViewer($viewer)
44 ->withPHIDs(array($operation_phid))
45 ->executeOne();
46 if (!$operation) {
47 throw new PhabricatorWorkerPermanentFailureException(
48 pht('No such operation "%s"!', $operation_phid));
49 }
50
51 return $operation;
52 }
53
54 protected function loadCommands($target_phid) {
55 $viewer = $this->getViewer();
56
57 $commands = id(new DrydockCommandQuery())
58 ->setViewer($viewer)
59 ->withTargetPHIDs(array($target_phid))
60 ->withConsumed(false)
61 ->execute();
62
63 $commands = msort($commands, 'getID');
64
65 return $commands;
66 }
67
68 protected function checkLeaseExpiration(DrydockLease $lease) {
69 $this->checkObjectExpiration($lease);
70 }
71
72 protected function checkResourceExpiration(DrydockResource $resource) {
73 $this->checkObjectExpiration($resource);
74 }
75
76 private function checkObjectExpiration($object) {
77 // Check if the resource or lease has expired. If it has, we're going to
78 // send it a release command.
79
80 // This command is sent from within the update worker so it is handled
81 // immediately, but doing this generates a log and improves consistency.
82
83 $expires = $object->getUntil();
84 if (!$expires) {
85 return;
86 }
87
88 $now = PhabricatorTime::getNow();
89 if ($expires > $now) {
90 return;
91 }
92
93 $viewer = $this->getViewer();
94 $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
95
96 $command = DrydockCommand::initializeNewCommand($viewer)
97 ->setTargetPHID($object->getPHID())
98 ->setAuthorPHID($drydock_phid)
99 ->setCommand(DrydockCommand::COMMAND_RELEASE)
100 ->save();
101 }
102
103 protected function yieldIfExpiringLease(DrydockLease $lease) {
104 if (!$lease->canReceiveCommands()) {
105 return;
106 }
107
108 $this->yieldIfExpiring($lease->getUntil());
109 }
110
111 protected function yieldIfExpiringResource(DrydockResource $resource) {
112 if (!$resource->canReceiveCommands()) {
113 return;
114 }
115
116 $this->yieldIfExpiring($resource->getUntil());
117 }
118
119 private function yieldIfExpiring($expires) {
120 if (!$expires) {
121 return;
122 }
123
124 if (!$this->getTaskDataValue('isExpireTask')) {
125 return;
126 }
127
128 $now = PhabricatorTime::getNow();
129 throw new PhabricatorWorkerYieldException($expires - $now);
130 }
131
132 protected function isTemporaryException(Exception $ex) {
133 if ($ex instanceof PhabricatorWorkerYieldException) {
134 return true;
135 }
136
137 if ($ex instanceof DrydockSlotLockException) {
138 return true;
139 }
140
141 if ($ex instanceof PhutilAggregateException) {
142 $any_temporary = false;
143 foreach ($ex->getExceptions() as $sub) {
144 if ($this->isTemporaryException($sub)) {
145 $any_temporary = true;
146 break;
147 }
148 }
149 if ($any_temporary) {
150 return true;
151 }
152 }
153
154 if ($ex->getPrevious()) {
155 return $this->isTemporaryException($ex->getPrevious());
156 }
157
158 return false;
159 }
160
161 protected function getYieldDurationFromException(Exception $ex) {
162 if ($ex instanceof PhabricatorWorkerYieldException) {
163 return $ex->getDuration();
164 }
165
166 if ($ex instanceof DrydockSlotLockException) {
167 return 5;
168 }
169
170 return 15;
171 }
172
173 protected function flushDrydockTaskQueue() {
174 // NOTE: By default, queued tasks are not scheduled if the current task
175 // fails. This is a good, safe default behavior. For example, it can
176 // protect us from executing side effect tasks too many times, like
177 // sending extra email.
178
179 // However, it is not the behavior we want in Drydock, because we queue
180 // followup tasks after lease and resource failures and want them to
181 // execute in order to clean things up.
182
183 // At least for now, we just explicitly flush the queue before exiting
184 // with a failure to make sure tasks get queued up properly.
185 try {
186 $this->flushTaskQueue();
187 } catch (Exception $ex) {
188 // If this fails, we want to swallow the exception so the caller throws
189 // the original error, since we're more likely to be able to understand
190 // and fix the problem if we have the original error than if we replace
191 // it with this one.
192 phlog($ex);
193 }
194
195 return $this;
196 }
197
198 protected function canReclaimResource(DrydockResource $resource) {
199 $viewer = $this->getViewer();
200
201 // Don't reclaim a resource if it has been updated recently. If two
202 // leases are fighting, we don't want them to keep reclaiming resources
203 // from one another forever without making progress, so make resources
204 // immune to reclamation for a little while after they activate or update.
205
206 $now = PhabricatorTime::getNow();
207 $max_epoch = ($now - phutil_units('3 minutes in seconds'));
208
209 // TODO: It would be nice to use a more narrow time here, like "last
210 // activation or lease release", but we don't currently store that
211 // anywhere.
212
213 $updated = $resource->getDateModified();
214 if ($updated > $max_epoch) {
215 return false;
216 }
217
218 $statuses = array(
219 DrydockLeaseStatus::STATUS_PENDING,
220 DrydockLeaseStatus::STATUS_ACQUIRED,
221 DrydockLeaseStatus::STATUS_ACTIVE,
222 DrydockLeaseStatus::STATUS_RELEASED,
223 DrydockLeaseStatus::STATUS_BROKEN,
224 );
225
226 // Don't reclaim resources that have any active leases.
227 $leases = id(new DrydockLeaseQuery())
228 ->setViewer($viewer)
229 ->withResourcePHIDs(array($resource->getPHID()))
230 ->withStatuses($statuses)
231 ->setLimit(1)
232 ->execute();
233 if ($leases) {
234 return false;
235 }
236
237 // See T13676. Don't reclaim a resource if a lease recently released.
238 $leases = id(new DrydockLeaseQuery())
239 ->setViewer($viewer)
240 ->withResourcePHIDs(array($resource->getPHID()))
241 ->withStatuses(
242 array(
243 DrydockLeaseStatus::STATUS_DESTROYED,
244 ))
245 ->withDateModifiedBetween($max_epoch, null)
246 ->setLimit(1)
247 ->execute();
248 if ($leases) {
249 return false;
250 }
251
252 return true;
253 }
254
255 protected function reclaimResource(
256 DrydockResource $resource,
257 DrydockLease $lease) {
258 $viewer = $this->getViewer();
259
260 // Mark the lease as reclaiming this resource. It won't be allowed to start
261 // another reclaim as long as this resource is still in the process of
262 // being reclaimed.
263 $lease->addReclaimedResourcePHIDs(array($resource->getPHID()));
264
265 // When the resource releases, we we want to reawaken this task since it
266 // should (usually) be able to start building a new resource right away.
267 $worker_task_id = $this->getCurrentWorkerTaskID();
268
269 $command = DrydockCommand::initializeNewCommand($viewer)
270 ->setTargetPHID($resource->getPHID())
271 ->setAuthorPHID($lease->getPHID())
272 ->setCommand(DrydockCommand::COMMAND_RECLAIM)
273 ->setProperty('awakenTaskIDs', array($worker_task_id));
274
275 $lease->openTransaction();
276 $lease->save();
277 $command->save();
278 $lease->saveTransaction();
279
280 $resource->scheduleUpdate();
281
282 return $this;
283 }
284
285}