@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
at recaptime-dev/main 348 lines 9.0 kB view raw
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}