@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
3final class PhutilDaemonPool extends Phobject {
4
5 private $properties = array();
6 private $commandLineArguments;
7
8 private $overseer;
9 private $daemons = array();
10 private $argv;
11
12 private $lastAutoscaleUpdate;
13 private $inShutdown;
14
15 private function __construct() {
16 // <empty>
17 }
18
19 public static function newFromConfig(array $config) {
20 PhutilTypeSpec::checkMap(
21 $config,
22 array(
23 'class' => 'string',
24 'label' => 'string',
25 'argv' => 'optional list<string>',
26 'load' => 'optional list<string>',
27 'log' => 'optional string|null',
28 'pool' => 'optional int',
29 'up' => 'optional int',
30 'down' => 'optional int',
31 'reserve' => 'optional int|float',
32 ));
33
34 $config = $config + array(
35 'argv' => array(),
36 'load' => array(),
37 'log' => null,
38 'pool' => 1,
39 'up' => 2,
40 'down' => 15,
41 'reserve' => 0,
42 );
43
44 $pool = new self();
45 $pool->properties = $config;
46
47 return $pool;
48 }
49
50 public function setOverseer(PhutilDaemonOverseer $overseer) {
51 $this->overseer = $overseer;
52 return $this;
53 }
54
55 public function getOverseer() {
56 return $this->overseer;
57 }
58
59 public function setCommandLineArguments(array $arguments) {
60 $this->commandLineArguments = $arguments;
61 return $this;
62 }
63
64 public function getCommandLineArguments() {
65 return $this->commandLineArguments;
66 }
67
68 private function shouldShutdown() {
69 return $this->inShutdown;
70 }
71
72 private function newDaemon() {
73 $config = $this->properties;
74
75 if (count($this->daemons)) {
76 $down_duration = $this->getPoolScaledownDuration();
77 } else {
78 // TODO: For now, never scale pools down to 0.
79 $down_duration = 0;
80 }
81
82 $forced_config = array(
83 'down' => $down_duration,
84 );
85
86 $config = $forced_config + $config;
87
88 $config = array_select_keys(
89 $config,
90 array(
91 'class',
92 'log',
93 'load',
94 'argv',
95 'down',
96 ));
97
98 $daemon = PhutilDaemonHandle::newFromConfig($config)
99 ->setDaemonPool($this)
100 ->setCommandLineArguments($this->getCommandLineArguments());
101
102 $daemon_id = $daemon->getDaemonID();
103 $this->daemons[$daemon_id] = $daemon;
104
105 $daemon->didLaunch();
106
107 return $daemon;
108 }
109
110 public function getDaemons() {
111 return $this->daemons;
112 }
113
114 public function didReceiveSignal($signal, $signo) {
115 switch ($signal) {
116 case PhutilDaemonOverseer::SIGNAL_GRACEFUL:
117 case PhutilDaemonOverseer::SIGNAL_TERMINATE:
118 $this->inShutdown = true;
119 break;
120 }
121
122 foreach ($this->getDaemons() as $daemon) {
123 switch ($signal) {
124 case PhutilDaemonOverseer::SIGNAL_NOTIFY:
125 $daemon->didReceiveNotifySignal($signo);
126 break;
127 case PhutilDaemonOverseer::SIGNAL_RELOAD:
128 $daemon->didReceiveReloadSignal($signo);
129 break;
130 case PhutilDaemonOverseer::SIGNAL_GRACEFUL:
131 $daemon->didReceiveGracefulSignal($signo);
132 break;
133 case PhutilDaemonOverseer::SIGNAL_TERMINATE:
134 $daemon->didReceiveTerminateSignal($signo);
135 break;
136 default:
137 throw new Exception(
138 pht(
139 'Unknown signal "%s" ("%d").',
140 $signal,
141 $signo));
142 }
143 }
144 }
145
146 public function getPoolLabel() {
147 return $this->getPoolProperty('label');
148 }
149
150 public function getPoolMaximumSize() {
151 return $this->getPoolProperty('pool');
152 }
153
154 public function getPoolScaleupDuration() {
155 return $this->getPoolProperty('up');
156 }
157
158 public function getPoolScaledownDuration() {
159 return $this->getPoolProperty('down');
160 }
161
162 public function getPoolMemoryReserve() {
163 return $this->getPoolProperty('reserve');
164 }
165
166 public function getPoolDaemonClass() {
167 return $this->getPoolProperty('class');
168 }
169
170 private function getPoolProperty($key) {
171 return idx($this->properties, $key);
172 }
173
174 public function updatePool() {
175 $daemons = $this->getDaemons();
176
177 foreach ($daemons as $key => $daemon) {
178 $daemon->update();
179
180 if ($daemon->isDone()) {
181 $daemon->didExit();
182
183 unset($this->daemons[$key]);
184
185 if ($this->shouldShutdown()) {
186 $this->logMessage(
187 'DOWN',
188 pht(
189 'Pool "%s" is exiting, with %s daemon(s) remaining.',
190 $this->getPoolLabel(),
191 new PhutilNumber(count($this->daemons))));
192 } else {
193 $this->logMessage(
194 'POOL',
195 pht(
196 'Autoscale pool "%s" scaled down to %s daemon(s).',
197 $this->getPoolLabel(),
198 new PhutilNumber(count($this->daemons))));
199 }
200 }
201 }
202
203 $this->updateAutoscale();
204 }
205
206 public function isHibernating() {
207 foreach ($this->getDaemons() as $daemon) {
208 if (!$daemon->isHibernating()) {
209 return false;
210 }
211 }
212
213 return true;
214 }
215
216 public function wakeFromHibernation() {
217 if (!$this->isHibernating()) {
218 return $this;
219 }
220
221 $this->logMessage(
222 'WAKE',
223 pht(
224 'Autoscale pool "%s" is being awakened from hibernation.',
225 $this->getPoolLabel()));
226
227 $did_wake_daemons = false;
228 foreach ($this->getDaemons() as $daemon) {
229 if ($daemon->isHibernating()) {
230 $daemon->wakeFromHibernation();
231 $did_wake_daemons = true;
232 }
233 }
234
235 if (!$did_wake_daemons) {
236 // TODO: Pools currently can't scale down to 0 daemons, but we should
237 // scale up immediately here once they can.
238 }
239
240 $this->updatePool();
241
242 return $this;
243 }
244
245 private function updateAutoscale() {
246 if ($this->shouldShutdown()) {
247 return;
248 }
249
250 // Don't try to autoscale more than once per second. This mostly stops the
251 // logs from getting flooded in verbose mode.
252 $now = time();
253 if ($this->lastAutoscaleUpdate >= $now) {
254 return;
255 }
256 $this->lastAutoscaleUpdate = $now;
257
258 $daemons = $this->getDaemons();
259
260 // If this pool is already at the maximum size, we can't launch any new
261 // daemons.
262 $max_size = $this->getPoolMaximumSize();
263 if (count($daemons) >= $max_size) {
264 $this->logMessage(
265 'POOL',
266 pht(
267 'Autoscale pool "%s" already at maximum size (%s of %s).',
268 $this->getPoolLabel(),
269 new PhutilNumber(count($daemons)),
270 new PhutilNumber($max_size)));
271 return;
272 }
273
274 $scaleup_duration = $this->getPoolScaleupDuration();
275
276 foreach ($daemons as $daemon) {
277 $busy_epoch = $daemon->getBusyEpoch();
278 // If any daemons haven't started work yet, don't scale the pool up.
279 if (!$busy_epoch) {
280 $this->logMessage(
281 'POOL',
282 pht(
283 'Autoscale pool "%s" has an idle daemon, declining to scale.',
284 $this->getPoolLabel()));
285 return;
286 }
287
288 // If any daemons started work very recently, wait a little while
289 // to scale the pool up.
290 $busy_for = ($now - $busy_epoch);
291 if ($busy_for < $scaleup_duration) {
292 $this->logMessage(
293 'POOL',
294 pht(
295 'Autoscale pool "%s" has not been busy long enough to scale up '.
296 '(busy for %s of %s seconds).',
297 $this->getPoolLabel(),
298 new PhutilNumber($busy_for),
299 new PhutilNumber($scaleup_duration)));
300 return;
301 }
302 }
303
304 // If we have a configured memory reserve for this pool, it tells us that
305 // we should not scale up unless there's at least that much memory left
306 // on the system (for example, a reserve of 0.25 means that 25% of system
307 // memory must be free to autoscale).
308
309 // Note that the first daemon is exempt: we'll always launch at least one
310 // daemon, regardless of any memory reservation.
311 if (count($daemons)) {
312 $reserve = $this->getPoolMemoryReserve();
313 if ($reserve) {
314 // On some systems this may be slightly more expensive than other
315 // checks, so we only do it once we're prepared to scale up.
316 $memory = PhutilSystem::getSystemMemoryInformation();
317 $free_ratio = ($memory['free'] / $memory['total']);
318
319 // If we don't have enough free memory, don't scale.
320 if ($free_ratio <= $reserve) {
321 $this->logMessage(
322 'POOL',
323 pht(
324 'Autoscale pool "%s" does not have enough free memory to '.
325 'scale up (%s free of %s reserved).',
326 $this->getPoolLabel(),
327 new PhutilNumber($free_ratio, 3),
328 new PhutilNumber($reserve, 3)));
329 return;
330 }
331 }
332 }
333
334 $this->logMessage(
335 'AUTO',
336 pht(
337 'Scaling pool "%s" up to %s daemon(s).',
338 $this->getPoolLabel(),
339 new PhutilNumber(count($daemons) + 1)));
340
341 $this->newDaemon();
342 }
343
344 public function logMessage($type, $message, $context = null) {
345 return $this->getOverseer()->logMessage($type, $message, $context);
346 }
347
348}