Page MenuHome GnuPG

No OneTemporary

diff --git a/scripts/daemon/exec/exec_daemon.php b/scripts/daemon/exec/exec_daemon.php
index 41da2c0..af53131 100755
--- a/scripts/daemon/exec/exec_daemon.php
+++ b/scripts/daemon/exec/exec_daemon.php
@@ -1,131 +1,131 @@
#!/usr/bin/env php
<?php
if (function_exists('pcntl_async_signals')) {
pcntl_async_signals(true);
} else {
declare(ticks = 1);
}
require_once dirname(__FILE__).'/../../__init_script__.php';
if (!posix_isatty(STDOUT)) {
$sid = posix_setsid();
if ($sid <= 0) {
throw new Exception(pht('Failed to create new process session!'));
}
}
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('daemon executor'));
$args->setSynopsis(<<<EOHELP
**exec_daemon.php** [__options__] __daemon__ ...
Run an instance of __daemon__.
EOHELP
);
$args->parse(
array(
array(
'name' => 'trace',
'help' => pht('Enable debug tracing.'),
),
array(
'name' => 'trace-memory',
'help' => pht('Enable debug memory tracing.'),
),
array(
'name' => 'verbose',
'help' => pht('Enable verbose activity logging.'),
),
array(
'name' => 'label',
'short' => 'l',
'param' => 'label',
'help' => pht(
'Optional process label. Makes "%s" nicer, no behavioral effects.',
'ps'),
),
array(
'name' => 'daemon',
'wildcard' => true,
),
));
$trace_memory = $args->getArg('trace-memory');
$trace_mode = $args->getArg('trace') || $trace_memory;
$verbose = $args->getArg('verbose');
if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n");
}
$config = @file_get_contents('php://stdin');
$config = id(new PhutilJSONParser())->parse($config);
PhutilTypeSpec::checkMap(
$config,
array(
'log' => 'optional string|null',
'argv' => 'optional list<wild>',
'load' => 'optional list<string>',
- 'autoscale' => 'optional wild',
+ 'down' => 'optional int',
));
$log = idx($config, 'log');
if ($log) {
ini_set('error_log', $log);
PhutilErrorHandler::setErrorListener(array('PhutilDaemon', 'errorListener'));
}
$load = idx($config, 'load', array());
foreach ($load as $library) {
$library = Filesystem::resolvePath($library);
phutil_load_library($library);
}
PhutilErrorHandler::initialize();
$daemon = $args->getArg('daemon');
if (!$daemon) {
throw new PhutilArgumentUsageException(
pht('Specify which class of daemon to start.'));
} else if (count($daemon) > 1) {
throw new PhutilArgumentUsageException(
pht('Specify exactly one daemon to start.'));
} else {
$daemon = head($daemon);
if (!class_exists($daemon)) {
throw new PhutilArgumentUsageException(
pht(
'No class "%s" exists in any known library.',
$daemon));
} else if (!is_subclass_of($daemon, 'PhutilDaemon')) {
throw new PhutilArgumentUsageException(
pht(
'Class "%s" is not a subclass of "%s".',
$daemon,
'PhutilDaemon'));
}
}
$argv = idx($config, 'argv', array());
$daemon = newv($daemon, array($argv));
if ($trace_mode) {
$daemon->setTraceMode();
}
if ($trace_memory) {
$daemon->setTraceMemory();
}
if ($verbose) {
$daemon->setVerbose(true);
}
-$autoscale = idx($config, 'autoscale');
-if ($autoscale) {
- $daemon->setAutoscaleProperties($autoscale);
+$down_duration = idx($config, 'down');
+if ($down_duration) {
+ $daemon->setScaledownDuration($down_duration);
}
$daemon->execute();
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 66dd81b..76d8676 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,1078 +1,1080 @@
<?php
/**
* This file is automatically generated. Use 'arc liberate' to rebuild it.
*
* @generated
* @phutil-library-version 2
*/
phutil_register_library_map(array(
'__library_version__' => 2,
'class' => array(
'AASTNode' => 'parser/aast/api/AASTNode.php',
'AASTNodeList' => 'parser/aast/api/AASTNodeList.php',
'AASTToken' => 'parser/aast/api/AASTToken.php',
'AASTTree' => 'parser/aast/api/AASTTree.php',
'AbstractDirectedGraph' => 'utils/AbstractDirectedGraph.php',
'AbstractDirectedGraphTestCase' => 'utils/__tests__/AbstractDirectedGraphTestCase.php',
'AphrontAccessDeniedQueryException' => 'aphront/storage/exception/AphrontAccessDeniedQueryException.php',
'AphrontBaseMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php',
'AphrontCharacterSetQueryException' => 'aphront/storage/exception/AphrontCharacterSetQueryException.php',
'AphrontConnectionLostQueryException' => 'aphront/storage/exception/AphrontConnectionLostQueryException.php',
'AphrontConnectionQueryException' => 'aphront/storage/exception/AphrontConnectionQueryException.php',
'AphrontCountQueryException' => 'aphront/storage/exception/AphrontCountQueryException.php',
'AphrontDatabaseConnection' => 'aphront/storage/connection/AphrontDatabaseConnection.php',
'AphrontDatabaseTransactionState' => 'aphront/storage/connection/AphrontDatabaseTransactionState.php',
'AphrontDeadlockQueryException' => 'aphront/storage/exception/AphrontDeadlockQueryException.php',
'AphrontDuplicateKeyQueryException' => 'aphront/storage/exception/AphrontDuplicateKeyQueryException.php',
'AphrontInvalidCredentialsQueryException' => 'aphront/storage/exception/AphrontInvalidCredentialsQueryException.php',
'AphrontIsolatedDatabaseConnection' => 'aphront/storage/connection/AphrontIsolatedDatabaseConnection.php',
'AphrontLockTimeoutQueryException' => 'aphront/storage/exception/AphrontLockTimeoutQueryException.php',
'AphrontMySQLDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLDatabaseConnection.php',
'AphrontMySQLiDatabaseConnection' => 'aphront/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php',
'AphrontNotSupportedQueryException' => 'aphront/storage/exception/AphrontNotSupportedQueryException.php',
'AphrontObjectMissingQueryException' => 'aphront/storage/exception/AphrontObjectMissingQueryException.php',
'AphrontParameterQueryException' => 'aphront/storage/exception/AphrontParameterQueryException.php',
'AphrontQueryException' => 'aphront/storage/exception/AphrontQueryException.php',
'AphrontQueryTimeoutQueryException' => 'aphront/storage/exception/AphrontQueryTimeoutQueryException.php',
'AphrontRecoverableQueryException' => 'aphront/storage/exception/AphrontRecoverableQueryException.php',
'AphrontRequestStream' => 'aphront/requeststream/AphrontRequestStream.php',
'AphrontSchemaQueryException' => 'aphront/storage/exception/AphrontSchemaQueryException.php',
'AphrontScopedUnguardedWriteCapability' => 'aphront/writeguard/AphrontScopedUnguardedWriteCapability.php',
'AphrontWriteGuard' => 'aphront/writeguard/AphrontWriteGuard.php',
'BaseHTTPFuture' => 'future/http/BaseHTTPFuture.php',
'CaseInsensitiveArray' => 'utils/CaseInsensitiveArray.php',
'CaseInsensitiveArrayTestCase' => 'utils/__tests__/CaseInsensitiveArrayTestCase.php',
'CommandException' => 'future/exec/CommandException.php',
'ConduitClient' => 'conduit/ConduitClient.php',
'ConduitClientException' => 'conduit/ConduitClientException.php',
'ConduitClientTestCase' => 'conduit/__tests__/ConduitClientTestCase.php',
'ConduitFuture' => 'conduit/ConduitFuture.php',
'ExecFuture' => 'future/exec/ExecFuture.php',
'ExecFutureTestCase' => 'future/exec/__tests__/ExecFutureTestCase.php',
'ExecPassthruTestCase' => 'future/exec/__tests__/ExecPassthruTestCase.php',
'FileFinder' => 'filesystem/FileFinder.php',
'FileFinderTestCase' => 'filesystem/__tests__/FileFinderTestCase.php',
'FileList' => 'filesystem/FileList.php',
'Filesystem' => 'filesystem/Filesystem.php',
'FilesystemException' => 'filesystem/FilesystemException.php',
'FilesystemTestCase' => 'filesystem/__tests__/FilesystemTestCase.php',
'Future' => 'future/Future.php',
'FutureIterator' => 'future/FutureIterator.php',
'FutureIteratorTestCase' => 'future/__tests__/FutureIteratorTestCase.php',
'FutureProxy' => 'future/FutureProxy.php',
'HTTPFuture' => 'future/http/HTTPFuture.php',
'HTTPFutureCURLResponseStatus' => 'future/http/status/HTTPFutureCURLResponseStatus.php',
'HTTPFutureCertificateResponseStatus' => 'future/http/status/HTTPFutureCertificateResponseStatus.php',
'HTTPFutureHTTPResponseStatus' => 'future/http/status/HTTPFutureHTTPResponseStatus.php',
'HTTPFutureParseResponseStatus' => 'future/http/status/HTTPFutureParseResponseStatus.php',
'HTTPFutureResponseStatus' => 'future/http/status/HTTPFutureResponseStatus.php',
'HTTPFutureTransportResponseStatus' => 'future/http/status/HTTPFutureTransportResponseStatus.php',
'HTTPSFuture' => 'future/http/HTTPSFuture.php',
'ImmediateFuture' => 'future/ImmediateFuture.php',
'LibphutilUSEnglishTranslation' => 'internationalization/translation/LibphutilUSEnglishTranslation.php',
'LinesOfALarge' => 'filesystem/linesofalarge/LinesOfALarge.php',
'LinesOfALargeExecFuture' => 'filesystem/linesofalarge/LinesOfALargeExecFuture.php',
'LinesOfALargeExecFutureTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeExecFutureTestCase.php',
'LinesOfALargeFile' => 'filesystem/linesofalarge/LinesOfALargeFile.php',
'LinesOfALargeFileTestCase' => 'filesystem/linesofalarge/__tests__/LinesOfALargeFileTestCase.php',
'MFilterTestHelper' => 'utils/__tests__/MFilterTestHelper.php',
'PHPASTParserTestCase' => 'parser/xhpast/__tests__/PHPASTParserTestCase.php',
'PhageAction' => 'phage/action/PhageAction.php',
'PhageAgentAction' => 'phage/action/PhageAgentAction.php',
'PhageAgentBootloader' => 'phage/bootloader/PhageAgentBootloader.php',
'PhageAgentTestCase' => 'phage/__tests__/PhageAgentTestCase.php',
'PhageExecuteAction' => 'phage/action/PhageExecuteAction.php',
'PhageLocalAction' => 'phage/action/PhageLocalAction.php',
'PhagePHPAgent' => 'phage/agent/PhagePHPAgent.php',
'PhagePHPAgentBootloader' => 'phage/bootloader/PhagePHPAgentBootloader.php',
'PhagePlanAction' => 'phage/action/PhagePlanAction.php',
'Phobject' => 'object/Phobject.php',
'PhobjectTestCase' => 'object/__tests__/PhobjectTestCase.php',
'PhutilAPCKeyValueCache' => 'cache/PhutilAPCKeyValueCache.php',
'PhutilAWSEC2Future' => 'future/aws/PhutilAWSEC2Future.php',
'PhutilAWSException' => 'future/aws/PhutilAWSException.php',
'PhutilAWSFuture' => 'future/aws/PhutilAWSFuture.php',
'PhutilAWSManagementWorkflow' => 'future/aws/management/PhutilAWSManagementWorkflow.php',
'PhutilAWSS3DeleteManagementWorkflow' => 'future/aws/management/PhutilAWSS3DeleteManagementWorkflow.php',
'PhutilAWSS3Future' => 'future/aws/PhutilAWSS3Future.php',
'PhutilAWSS3GetManagementWorkflow' => 'future/aws/management/PhutilAWSS3GetManagementWorkflow.php',
'PhutilAWSS3ManagementWorkflow' => 'future/aws/management/PhutilAWSS3ManagementWorkflow.php',
'PhutilAWSS3PutManagementWorkflow' => 'future/aws/management/PhutilAWSS3PutManagementWorkflow.php',
'PhutilAWSv4Signature' => 'future/aws/PhutilAWSv4Signature.php',
'PhutilAWSv4SignatureTestCase' => 'future/aws/__tests__/PhutilAWSv4SignatureTestCase.php',
'PhutilAggregateException' => 'error/PhutilAggregateException.php',
'PhutilAllCapsEnglishLocale' => 'internationalization/locales/PhutilAllCapsEnglishLocale.php',
'PhutilAmazonAuthAdapter' => 'auth/PhutilAmazonAuthAdapter.php',
'PhutilArgumentParser' => 'parser/argument/PhutilArgumentParser.php',
'PhutilArgumentParserException' => 'parser/argument/exception/PhutilArgumentParserException.php',
'PhutilArgumentParserTestCase' => 'parser/argument/__tests__/PhutilArgumentParserTestCase.php',
'PhutilArgumentSpecification' => 'parser/argument/PhutilArgumentSpecification.php',
'PhutilArgumentSpecificationException' => 'parser/argument/exception/PhutilArgumentSpecificationException.php',
'PhutilArgumentSpecificationTestCase' => 'parser/argument/__tests__/PhutilArgumentSpecificationTestCase.php',
'PhutilArgumentSpellingCorrector' => 'parser/argument/PhutilArgumentSpellingCorrector.php',
'PhutilArgumentSpellingCorrectorTestCase' => 'parser/argument/__tests__/PhutilArgumentSpellingCorrectorTestCase.php',
'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
'PhutilArray' => 'utils/PhutilArray.php',
'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
'PhutilAsanaAuthAdapter' => 'auth/PhutilAsanaAuthAdapter.php',
'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
'PhutilAuthAdapter' => 'auth/PhutilAuthAdapter.php',
'PhutilAuthConfigurationException' => 'auth/exception/PhutilAuthConfigurationException.php',
'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php',
'PhutilAuthException' => 'auth/exception/PhutilAuthException.php',
'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php',
'PhutilBacktraceSignalHandler' => 'future/exec/PhutilBacktraceSignalHandler.php',
'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php',
'PhutilBitbucketAuthAdapter' => 'auth/PhutilBitbucketAuthAdapter.php',
'PhutilBootloader' => 'moduleutils/PhutilBootloader.php',
'PhutilBootloaderException' => 'moduleutils/PhutilBootloaderException.php',
'PhutilBritishEnglishLocale' => 'internationalization/locales/PhutilBritishEnglishLocale.php',
'PhutilBufferedIterator' => 'utils/PhutilBufferedIterator.php',
'PhutilBufferedIteratorTestCase' => 'utils/__tests__/PhutilBufferedIteratorTestCase.php',
'PhutilBugtraqParser' => 'parser/PhutilBugtraqParser.php',
'PhutilBugtraqParserTestCase' => 'parser/__tests__/PhutilBugtraqParserTestCase.php',
'PhutilCIDRBlock' => 'ip/PhutilCIDRBlock.php',
'PhutilCIDRList' => 'ip/PhutilCIDRList.php',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCLikeCodeSnippetContextFreeGrammar.php',
'PhutilCalendarAbsoluteDateTime' => 'parser/calendar/data/PhutilCalendarAbsoluteDateTime.php',
'PhutilCalendarContainerNode' => 'parser/calendar/data/PhutilCalendarContainerNode.php',
'PhutilCalendarDateTime' => 'parser/calendar/data/PhutilCalendarDateTime.php',
'PhutilCalendarDateTimeTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarDateTimeTestCase.php',
'PhutilCalendarDocumentNode' => 'parser/calendar/data/PhutilCalendarDocumentNode.php',
'PhutilCalendarDuration' => 'parser/calendar/data/PhutilCalendarDuration.php',
'PhutilCalendarEventNode' => 'parser/calendar/data/PhutilCalendarEventNode.php',
'PhutilCalendarNode' => 'parser/calendar/data/PhutilCalendarNode.php',
'PhutilCalendarProxyDateTime' => 'parser/calendar/data/PhutilCalendarProxyDateTime.php',
'PhutilCalendarRawNode' => 'parser/calendar/data/PhutilCalendarRawNode.php',
'PhutilCalendarRecurrenceList' => 'parser/calendar/data/PhutilCalendarRecurrenceList.php',
'PhutilCalendarRecurrenceRule' => 'parser/calendar/data/PhutilCalendarRecurrenceRule.php',
'PhutilCalendarRecurrenceRuleTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceRuleTestCase.php',
'PhutilCalendarRecurrenceSet' => 'parser/calendar/data/PhutilCalendarRecurrenceSet.php',
'PhutilCalendarRecurrenceSource' => 'parser/calendar/data/PhutilCalendarRecurrenceSource.php',
'PhutilCalendarRecurrenceTestCase' => 'parser/calendar/data/__tests__/PhutilCalendarRecurrenceTestCase.php',
'PhutilCalendarRelativeDateTime' => 'parser/calendar/data/PhutilCalendarRelativeDateTime.php',
'PhutilCalendarRootNode' => 'parser/calendar/data/PhutilCalendarRootNode.php',
'PhutilCalendarUserNode' => 'parser/calendar/data/PhutilCalendarUserNode.php',
'PhutilCallbackFilterIterator' => 'utils/PhutilCallbackFilterIterator.php',
'PhutilCallbackSignalHandler' => 'future/exec/PhutilCallbackSignalHandler.php',
'PhutilChannel' => 'channel/PhutilChannel.php',
'PhutilChannelChannel' => 'channel/PhutilChannelChannel.php',
'PhutilChannelTestCase' => 'channel/__tests__/PhutilChannelTestCase.php',
'PhutilChunkedIterator' => 'utils/PhutilChunkedIterator.php',
'PhutilChunkedIteratorTestCase' => 'utils/__tests__/PhutilChunkedIteratorTestCase.php',
'PhutilClassMapQuery' => 'symbols/PhutilClassMapQuery.php',
'PhutilCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilCodeSnippetContextFreeGrammar.php',
'PhutilCommandString' => 'xsprintf/PhutilCommandString.php',
'PhutilConsole' => 'console/PhutilConsole.php',
'PhutilConsoleBlock' => 'console/view/PhutilConsoleBlock.php',
'PhutilConsoleFormatter' => 'console/PhutilConsoleFormatter.php',
'PhutilConsoleList' => 'console/view/PhutilConsoleList.php',
'PhutilConsoleMessage' => 'console/PhutilConsoleMessage.php',
'PhutilConsoleProgressBar' => 'console/PhutilConsoleProgressBar.php',
'PhutilConsoleServer' => 'console/PhutilConsoleServer.php',
'PhutilConsoleServerChannel' => 'console/PhutilConsoleServerChannel.php',
'PhutilConsoleStdinNotInteractiveException' => 'console/PhutilConsoleStdinNotInteractiveException.php',
'PhutilConsoleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilConsoleSyntaxHighlighter.php',
'PhutilConsoleTable' => 'console/view/PhutilConsoleTable.php',
'PhutilConsoleView' => 'console/view/PhutilConsoleView.php',
'PhutilConsoleWrapTestCase' => 'console/__tests__/PhutilConsoleWrapTestCase.php',
'PhutilContextFreeGrammar' => 'grammar/PhutilContextFreeGrammar.php',
'PhutilCowsay' => 'utils/PhutilCowsay.php',
'PhutilCowsayTestCase' => 'utils/__tests__/PhutilCowsayTestCase.php',
'PhutilCsprintfTestCase' => 'xsprintf/__tests__/PhutilCsprintfTestCase.php',
'PhutilCzechLocale' => 'internationalization/locales/PhutilCzechLocale.php',
'PhutilDaemon' => 'daemon/PhutilDaemon.php',
'PhutilDaemonHandle' => 'daemon/PhutilDaemonHandle.php',
'PhutilDaemonOverseer' => 'daemon/PhutilDaemonOverseer.php',
'PhutilDaemonOverseerModule' => 'daemon/PhutilDaemonOverseerModule.php',
+ 'PhutilDaemonPool' => 'daemon/PhutilDaemonPool.php',
'PhutilDefaultSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDefaultSyntaxHighlighter.php',
'PhutilDefaultSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilDefaultSyntaxHighlighterEngine.php',
'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'markup/syntax/highlighter/pygments/PhutilDefaultSyntaxHighlighterEnginePygmentsFuture.php',
'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'markup/syntax/engine/__tests__/PhutilDefaultSyntaxHighlighterEngineTestCase.php',
'PhutilDeferredLog' => 'filesystem/PhutilDeferredLog.php',
'PhutilDeferredLogTestCase' => 'filesystem/__tests__/PhutilDeferredLogTestCase.php',
'PhutilDirectedScalarGraph' => 'utils/PhutilDirectedScalarGraph.php',
'PhutilDirectoryFixture' => 'filesystem/PhutilDirectoryFixture.php',
'PhutilDirectoryKeyValueCache' => 'cache/PhutilDirectoryKeyValueCache.php',
'PhutilDisqusAuthAdapter' => 'auth/PhutilDisqusAuthAdapter.php',
'PhutilDivinerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilDivinerSyntaxHighlighter.php',
'PhutilDocblockParser' => 'parser/PhutilDocblockParser.php',
'PhutilDocblockParserTestCase' => 'parser/__tests__/PhutilDocblockParserTestCase.php',
'PhutilEditDistanceMatrix' => 'utils/PhutilEditDistanceMatrix.php',
'PhutilEditDistanceMatrixTestCase' => 'utils/__tests__/PhutilEditDistanceMatrixTestCase.php',
'PhutilEditorConfig' => 'parser/PhutilEditorConfig.php',
'PhutilEditorConfigTestCase' => 'parser/__tests__/PhutilEditorConfigTestCase.php',
'PhutilEmailAddress' => 'parser/PhutilEmailAddress.php',
'PhutilEmailAddressTestCase' => 'parser/__tests__/PhutilEmailAddressTestCase.php',
'PhutilEmojiLocale' => 'internationalization/locales/PhutilEmojiLocale.php',
'PhutilEmptyAuthAdapter' => 'auth/PhutilEmptyAuthAdapter.php',
'PhutilEnglishCanadaLocale' => 'internationalization/locales/PhutilEnglishCanadaLocale.php',
'PhutilErrorHandler' => 'error/PhutilErrorHandler.php',
'PhutilErrorHandlerTestCase' => 'error/__tests__/PhutilErrorHandlerTestCase.php',
'PhutilErrorTrap' => 'error/PhutilErrorTrap.php',
'PhutilEvent' => 'events/PhutilEvent.php',
'PhutilEventConstants' => 'events/constant/PhutilEventConstants.php',
'PhutilEventEngine' => 'events/PhutilEventEngine.php',
'PhutilEventListener' => 'events/PhutilEventListener.php',
'PhutilEventType' => 'events/constant/PhutilEventType.php',
'PhutilExampleBufferedIterator' => 'utils/PhutilExampleBufferedIterator.php',
'PhutilExcessiveServiceCallsDaemon' => 'daemon/torture/PhutilExcessiveServiceCallsDaemon.php',
'PhutilExecChannel' => 'channel/PhutilExecChannel.php',
'PhutilExecPassthru' => 'future/exec/PhutilExecPassthru.php',
'PhutilExecutableFuture' => 'future/exec/PhutilExecutableFuture.php',
'PhutilExecutionEnvironment' => 'utils/PhutilExecutionEnvironment.php',
'PhutilExtensionsTestCase' => 'moduleutils/__tests__/PhutilExtensionsTestCase.php',
'PhutilFacebookAuthAdapter' => 'auth/PhutilFacebookAuthAdapter.php',
'PhutilFatalDaemon' => 'daemon/torture/PhutilFatalDaemon.php',
'PhutilFileLock' => 'filesystem/PhutilFileLock.php',
'PhutilFileLockTestCase' => 'filesystem/__tests__/PhutilFileLockTestCase.php',
'PhutilFileTree' => 'filesystem/PhutilFileTree.php',
'PhutilFrenchLocale' => 'internationalization/locales/PhutilFrenchLocale.php',
'PhutilGermanLocale' => 'internationalization/locales/PhutilGermanLocale.php',
'PhutilGitHubAuthAdapter' => 'auth/PhutilGitHubAuthAdapter.php',
'PhutilGitHubFuture' => 'future/github/PhutilGitHubFuture.php',
'PhutilGitHubResponse' => 'future/github/PhutilGitHubResponse.php',
'PhutilGitURI' => 'parser/PhutilGitURI.php',
'PhutilGitURITestCase' => 'parser/__tests__/PhutilGitURITestCase.php',
'PhutilGoogleAuthAdapter' => 'auth/PhutilGoogleAuthAdapter.php',
'PhutilHTTPEngineExtension' => 'future/http/PhutilHTTPEngineExtension.php',
'PhutilHangForeverDaemon' => 'daemon/torture/PhutilHangForeverDaemon.php',
'PhutilHashingIterator' => 'utils/PhutilHashingIterator.php',
'PhutilHashingIteratorTestCase' => 'utils/__tests__/PhutilHashingIteratorTestCase.php',
'PhutilHelpArgumentWorkflow' => 'parser/argument/workflow/PhutilHelpArgumentWorkflow.php',
'PhutilHgsprintfTestCase' => 'xsprintf/__tests__/PhutilHgsprintfTestCase.php',
'PhutilHighIntensityIntervalDaemon' => 'daemon/torture/PhutilHighIntensityIntervalDaemon.php',
'PhutilICSParser' => 'parser/calendar/ics/PhutilICSParser.php',
'PhutilICSParserException' => 'parser/calendar/ics/PhutilICSParserException.php',
'PhutilICSParserTestCase' => 'parser/calendar/ics/__tests__/PhutilICSParserTestCase.php',
'PhutilICSWriter' => 'parser/calendar/ics/PhutilICSWriter.php',
'PhutilICSWriterTestCase' => 'parser/calendar/ics/__tests__/PhutilICSWriterTestCase.php',
'PhutilINIParserException' => 'parser/exception/PhutilINIParserException.php',
'PhutilIPAddress' => 'ip/PhutilIPAddress.php',
'PhutilIPAddressTestCase' => 'ip/__tests__/PhutilIPAddressTestCase.php',
'PhutilIPv4Address' => 'ip/PhutilIPv4Address.php',
'PhutilIPv6Address' => 'ip/PhutilIPv6Address.php',
'PhutilInRequestKeyValueCache' => 'cache/PhutilInRequestKeyValueCache.php',
'PhutilInteractiveEditor' => 'console/PhutilInteractiveEditor.php',
'PhutilInvalidRuleParserGeneratorException' => 'parser/generator/exception/PhutilInvalidRuleParserGeneratorException.php',
'PhutilInvalidStateException' => 'exception/PhutilInvalidStateException.php',
'PhutilInvalidStateExceptionTestCase' => 'exception/__tests__/PhutilInvalidStateExceptionTestCase.php',
'PhutilInvisibleSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilInvisibleSyntaxHighlighter.php',
'PhutilIrreducibleRuleParserGeneratorException' => 'parser/generator/exception/PhutilIrreducibleRuleParserGeneratorException.php',
'PhutilJIRAAuthAdapter' => 'auth/PhutilJIRAAuthAdapter.php',
'PhutilJSON' => 'parser/PhutilJSON.php',
'PhutilJSONFragmentLexer' => 'lexer/PhutilJSONFragmentLexer.php',
'PhutilJSONFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilJSONFragmentLexerHighlighterTestCase.php',
'PhutilJSONParser' => 'parser/PhutilJSONParser.php',
'PhutilJSONParserException' => 'parser/exception/PhutilJSONParserException.php',
'PhutilJSONParserTestCase' => 'parser/__tests__/PhutilJSONParserTestCase.php',
'PhutilJSONProtocolChannel' => 'channel/PhutilJSONProtocolChannel.php',
'PhutilJSONProtocolChannelTestCase' => 'channel/__tests__/PhutilJSONProtocolChannelTestCase.php',
'PhutilJSONTestCase' => 'parser/__tests__/PhutilJSONTestCase.php',
'PhutilJavaCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilJavaCodeSnippetContextFreeGrammar.php',
'PhutilKeyValueCache' => 'cache/PhutilKeyValueCache.php',
'PhutilKeyValueCacheNamespace' => 'cache/PhutilKeyValueCacheNamespace.php',
'PhutilKeyValueCacheProfiler' => 'cache/PhutilKeyValueCacheProfiler.php',
'PhutilKeyValueCacheProxy' => 'cache/PhutilKeyValueCacheProxy.php',
'PhutilKeyValueCacheStack' => 'cache/PhutilKeyValueCacheStack.php',
'PhutilKeyValueCacheTestCase' => 'cache/__tests__/PhutilKeyValueCacheTestCase.php',
'PhutilKoreanLocale' => 'internationalization/locales/PhutilKoreanLocale.php',
'PhutilLDAPAuthAdapter' => 'auth/PhutilLDAPAuthAdapter.php',
'PhutilLanguageGuesser' => 'parser/PhutilLanguageGuesser.php',
'PhutilLanguageGuesserTestCase' => 'parser/__tests__/PhutilLanguageGuesserTestCase.php',
'PhutilLexer' => 'lexer/PhutilLexer.php',
'PhutilLexerSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilLexerSyntaxHighlighter.php',
'PhutilLibraryConflictException' => 'moduleutils/PhutilLibraryConflictException.php',
'PhutilLibraryMapBuilder' => 'moduleutils/PhutilLibraryMapBuilder.php',
'PhutilLibraryTestCase' => '__tests__/PhutilLibraryTestCase.php',
'PhutilLipsumContextFreeGrammar' => 'grammar/PhutilLipsumContextFreeGrammar.php',
'PhutilLocale' => 'internationalization/PhutilLocale.php',
'PhutilLocaleTestCase' => 'internationalization/__tests__/PhutilLocaleTestCase.php',
'PhutilLock' => 'filesystem/PhutilLock.php',
'PhutilLockException' => 'filesystem/PhutilLockException.php',
'PhutilLogFileChannel' => 'channel/PhutilLogFileChannel.php',
'PhutilLunarPhase' => 'utils/PhutilLunarPhase.php',
'PhutilLunarPhaseTestCase' => 'utils/__tests__/PhutilLunarPhaseTestCase.php',
'PhutilMarkupEngine' => 'markup/PhutilMarkupEngine.php',
'PhutilMarkupTestCase' => 'markup/__tests__/PhutilMarkupTestCase.php',
'PhutilMemcacheKeyValueCache' => 'cache/PhutilMemcacheKeyValueCache.php',
'PhutilMethodNotImplementedException' => 'error/PhutilMethodNotImplementedException.php',
'PhutilMetricsChannel' => 'channel/PhutilMetricsChannel.php',
'PhutilMissingSymbolException' => 'symbols/exception/PhutilMissingSymbolException.php',
'PhutilModuleUtilsTestCase' => 'moduleutils/__tests__/PhutilModuleUtilsTestCase.php',
'PhutilNiceDaemon' => 'daemon/torture/PhutilNiceDaemon.php',
'PhutilNumber' => 'internationalization/PhutilNumber.php',
'PhutilOAuth1AuthAdapter' => 'auth/PhutilOAuth1AuthAdapter.php',
'PhutilOAuth1Future' => 'future/oauth/PhutilOAuth1Future.php',
'PhutilOAuth1FutureTestCase' => 'future/oauth/__tests__/PhutilOAuth1FutureTestCase.php',
'PhutilOAuthAuthAdapter' => 'auth/PhutilOAuthAuthAdapter.php',
'PhutilOnDiskKeyValueCache' => 'cache/PhutilOnDiskKeyValueCache.php',
'PhutilOpaqueEnvelope' => 'error/PhutilOpaqueEnvelope.php',
'PhutilOpaqueEnvelopeKey' => 'error/PhutilOpaqueEnvelopeKey.php',
'PhutilOpaqueEnvelopeTestCase' => 'error/__tests__/PhutilOpaqueEnvelopeTestCase.php',
'PhutilPHPCodeSnippetContextFreeGrammar' => 'grammar/code/PhutilPHPCodeSnippetContextFreeGrammar.php',
'PhutilPHPFragmentLexer' => 'lexer/PhutilPHPFragmentLexer.php',
'PhutilPHPFragmentLexerHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilPHPFragmentLexerHighlighterTestCase.php',
'PhutilPHPFragmentLexerTestCase' => 'lexer/__tests__/PhutilPHPFragmentLexerTestCase.php',
'PhutilPHPObjectProtocolChannel' => 'channel/PhutilPHPObjectProtocolChannel.php',
'PhutilPHPObjectProtocolChannelTestCase' => 'channel/__tests__/PhutilPHPObjectProtocolChannelTestCase.php',
'PhutilParserGenerator' => 'parser/PhutilParserGenerator.php',
'PhutilParserGeneratorException' => 'parser/generator/exception/PhutilParserGeneratorException.php',
'PhutilParserGeneratorTestCase' => 'parser/__tests__/PhutilParserGeneratorTestCase.php',
'PhutilPayPalAPIFuture' => 'future/paypal/PhutilPayPalAPIFuture.php',
'PhutilPerson' => 'internationalization/PhutilPerson.php',
'PhutilPersonTest' => 'internationalization/__tests__/PhutilPersonTest.php',
'PhutilPhabricatorAuthAdapter' => 'auth/PhutilPhabricatorAuthAdapter.php',
'PhutilPhtTestCase' => 'internationalization/__tests__/PhutilPhtTestCase.php',
'PhutilPirateEnglishLocale' => 'internationalization/locales/PhutilPirateEnglishLocale.php',
'PhutilPortugueseBrazilLocale' => 'internationalization/locales/PhutilPortugueseBrazilLocale.php',
'PhutilPortuguesePortugalLocale' => 'internationalization/locales/PhutilPortuguesePortugalLocale.php',
'PhutilPregsprintfTestCase' => 'xsprintf/__tests__/PhutilPregsprintfTestCase.php',
'PhutilProcessGroupDaemon' => 'daemon/torture/PhutilProcessGroupDaemon.php',
'PhutilProseDiff' => 'utils/PhutilProseDiff.php',
'PhutilProseDiffTestCase' => 'utils/__tests__/PhutilProseDiffTestCase.php',
'PhutilProseDifferenceEngine' => 'utils/PhutilProseDifferenceEngine.php',
'PhutilProtocolChannel' => 'channel/PhutilProtocolChannel.php',
'PhutilProxyException' => 'error/PhutilProxyException.php',
'PhutilProxyIterator' => 'utils/PhutilProxyIterator.php',
'PhutilPygmentizeParser' => 'parser/PhutilPygmentizeParser.php',
'PhutilPygmentizeParserTestCase' => 'parser/__tests__/PhutilPygmentizeParserTestCase.php',
'PhutilPygmentsSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilPygmentsSyntaxHighlighter.php',
'PhutilPythonFragmentLexer' => 'lexer/PhutilPythonFragmentLexer.php',
'PhutilQsprintfInterface' => 'xsprintf/PhutilQsprintfInterface.php',
'PhutilQueryStringParser' => 'parser/PhutilQueryStringParser.php',
'PhutilQueryStringParserTestCase' => 'parser/__tests__/PhutilQueryStringParserTestCase.php',
'PhutilRainbowSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilRainbowSyntaxHighlighter.php',
'PhutilRawEnglishLocale' => 'internationalization/locales/PhutilRawEnglishLocale.php',
'PhutilReadableSerializer' => 'readableserializer/PhutilReadableSerializer.php',
'PhutilReadableSerializerTestCase' => 'readableserializer/__tests__/PhutilReadableSerializerTestCase.php',
'PhutilRealNameContextFreeGrammar' => 'grammar/PhutilRealNameContextFreeGrammar.php',
'PhutilRemarkupBlockInterpreter' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockInterpreter.php',
'PhutilRemarkupBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupBlockRule.php',
'PhutilRemarkupBlockStorage' => 'markup/engine/remarkup/PhutilRemarkupBlockStorage.php',
'PhutilRemarkupBoldRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupBoldRule.php',
'PhutilRemarkupCodeBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupCodeBlockRule.php',
'PhutilRemarkupDefaultBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupDefaultBlockRule.php',
'PhutilRemarkupDelRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDelRule.php',
'PhutilRemarkupDocumentLinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupDocumentLinkRule.php',
'PhutilRemarkupEngine' => 'markup/engine/PhutilRemarkupEngine.php',
'PhutilRemarkupEngineTestCase' => 'markup/engine/__tests__/PhutilRemarkupEngineTestCase.php',
'PhutilRemarkupEscapeRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupEscapeRemarkupRule.php',
'PhutilRemarkupHeaderBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHeaderBlockRule.php',
'PhutilRemarkupHighlightRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHighlightRule.php',
'PhutilRemarkupHorizontalRuleBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupHorizontalRuleBlockRule.php',
'PhutilRemarkupHyperlinkRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupHyperlinkRule.php',
'PhutilRemarkupInlineBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInlineBlockRule.php',
'PhutilRemarkupInterpreterBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupInterpreterBlockRule.php',
'PhutilRemarkupItalicRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupItalicRule.php',
'PhutilRemarkupLinebreaksRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupLinebreaksRule.php',
'PhutilRemarkupListBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupListBlockRule.php',
'PhutilRemarkupLiteralBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupLiteralBlockRule.php',
'PhutilRemarkupMonospaceRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupMonospaceRule.php',
'PhutilRemarkupNoteBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupNoteBlockRule.php',
'PhutilRemarkupQuotesBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupQuotesBlockRule.php',
'PhutilRemarkupReplyBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupReplyBlockRule.php',
'PhutilRemarkupRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupRule.php',
'PhutilRemarkupSimpleTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupSimpleTableBlockRule.php',
'PhutilRemarkupTableBlockRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTableBlockRule.php',
'PhutilRemarkupTestInterpreterRule' => 'markup/engine/remarkup/blockrule/PhutilRemarkupTestInterpreterRule.php',
'PhutilRemarkupUnderlineRule' => 'markup/engine/remarkup/markuprule/PhutilRemarkupUnderlineRule.php',
'PhutilRope' => 'utils/PhutilRope.php',
'PhutilRopeTestCase' => 'utils/__tests__/PhutilRopeTestCase.php',
'PhutilSafeHTML' => 'markup/PhutilSafeHTML.php',
'PhutilSafeHTMLProducerInterface' => 'markup/PhutilSafeHTMLProducerInterface.php',
'PhutilSafeHTMLTestCase' => 'markup/__tests__/PhutilSafeHTMLTestCase.php',
'PhutilSaturateStdoutDaemon' => 'daemon/torture/PhutilSaturateStdoutDaemon.php',
'PhutilSearchQueryCompiler' => 'search/PhutilSearchQueryCompiler.php',
'PhutilSearchQueryCompilerSyntaxException' => 'search/PhutilSearchQueryCompilerSyntaxException.php',
'PhutilSearchQueryCompilerTestCase' => 'search/__tests__/PhutilSearchQueryCompilerTestCase.php',
'PhutilSearchStemmer' => 'search/PhutilSearchStemmer.php',
'PhutilSearchStemmerTestCase' => 'search/__tests__/PhutilSearchStemmerTestCase.php',
'PhutilServiceProfiler' => 'serviceprofiler/PhutilServiceProfiler.php',
'PhutilShellLexer' => 'lexer/PhutilShellLexer.php',
'PhutilShellLexerTestCase' => 'lexer/__tests__/PhutilShellLexerTestCase.php',
'PhutilSignalHandler' => 'future/exec/PhutilSignalHandler.php',
'PhutilSignalRouter' => 'future/exec/PhutilSignalRouter.php',
'PhutilSimpleOptions' => 'parser/PhutilSimpleOptions.php',
'PhutilSimpleOptionsLexer' => 'lexer/PhutilSimpleOptionsLexer.php',
'PhutilSimpleOptionsLexerTestCase' => 'lexer/__tests__/PhutilSimpleOptionsLexerTestCase.php',
'PhutilSimpleOptionsTestCase' => 'parser/__tests__/PhutilSimpleOptionsTestCase.php',
'PhutilSimplifiedChineseLocale' => 'internationalization/locales/PhutilSimplifiedChineseLocale.php',
'PhutilSlackAuthAdapter' => 'auth/PhutilSlackAuthAdapter.php',
'PhutilSlackFuture' => 'future/slack/PhutilSlackFuture.php',
'PhutilSocketChannel' => 'channel/PhutilSocketChannel.php',
'PhutilSortVector' => 'utils/PhutilSortVector.php',
'PhutilSpanishSpainLocale' => 'internationalization/locales/PhutilSpanishSpainLocale.php',
'PhutilSprite' => 'sprites/PhutilSprite.php',
'PhutilSpriteSheet' => 'sprites/PhutilSpriteSheet.php',
'PhutilStreamIterator' => 'utils/PhutilStreamIterator.php',
'PhutilSymbolLoader' => 'symbols/PhutilSymbolLoader.php',
'PhutilSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilSyntaxHighlighter.php',
'PhutilSyntaxHighlighterEngine' => 'markup/syntax/engine/PhutilSyntaxHighlighterEngine.php',
'PhutilSyntaxHighlighterException' => 'markup/syntax/highlighter/PhutilSyntaxHighlighterException.php',
'PhutilSystem' => 'utils/PhutilSystem.php',
'PhutilSystemTestCase' => 'utils/__tests__/PhutilSystemTestCase.php',
'PhutilTerminalString' => 'xsprintf/PhutilTerminalString.php',
'PhutilTestPhobject' => 'object/__tests__/PhutilTestPhobject.php',
'PhutilTortureTestDaemon' => 'daemon/torture/PhutilTortureTestDaemon.php',
'PhutilTraditionalChineseLocale' => 'internationalization/locales/PhutilTraditionalChineseLocale.php',
'PhutilTranslation' => 'internationalization/PhutilTranslation.php',
'PhutilTranslationTestCase' => 'internationalization/__tests__/PhutilTranslationTestCase.php',
'PhutilTranslator' => 'internationalization/PhutilTranslator.php',
'PhutilTranslatorTestCase' => 'internationalization/__tests__/PhutilTranslatorTestCase.php',
'PhutilTsprintfTestCase' => 'xsprintf/__tests__/PhutilTsprintfTestCase.php',
'PhutilTwitchAuthAdapter' => 'auth/PhutilTwitchAuthAdapter.php',
'PhutilTwitchFuture' => 'future/twitch/PhutilTwitchFuture.php',
'PhutilTwitterAuthAdapter' => 'auth/PhutilTwitterAuthAdapter.php',
'PhutilTypeCheckException' => 'parser/exception/PhutilTypeCheckException.php',
'PhutilTypeExtraParametersException' => 'parser/exception/PhutilTypeExtraParametersException.php',
'PhutilTypeLexer' => 'lexer/PhutilTypeLexer.php',
'PhutilTypeMissingParametersException' => 'parser/exception/PhutilTypeMissingParametersException.php',
'PhutilTypeSpec' => 'parser/PhutilTypeSpec.php',
'PhutilTypeSpecTestCase' => 'parser/__tests__/PhutilTypeSpecTestCase.php',
'PhutilURI' => 'parser/PhutilURI.php',
'PhutilURITestCase' => 'parser/__tests__/PhutilURITestCase.php',
'PhutilUSEnglishLocale' => 'internationalization/locales/PhutilUSEnglishLocale.php',
'PhutilUTF8StringTruncator' => 'utils/PhutilUTF8StringTruncator.php',
'PhutilUTF8TestCase' => 'utils/__tests__/PhutilUTF8TestCase.php',
'PhutilUnknownSymbolParserGeneratorException' => 'parser/generator/exception/PhutilUnknownSymbolParserGeneratorException.php',
'PhutilUnreachableRuleParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableRuleParserGeneratorException.php',
'PhutilUnreachableTerminalParserGeneratorException' => 'parser/generator/exception/PhutilUnreachableTerminalParserGeneratorException.php',
'PhutilUrisprintfTestCase' => 'xsprintf/__tests__/PhutilUrisprintfTestCase.php',
'PhutilUtilsTestCase' => 'utils/__tests__/PhutilUtilsTestCase.php',
'PhutilVeryWowEnglishLocale' => 'internationalization/locales/PhutilVeryWowEnglishLocale.php',
'PhutilWordPressAuthAdapter' => 'auth/PhutilWordPressAuthAdapter.php',
'PhutilWordPressFuture' => 'future/wordpress/PhutilWordPressFuture.php',
'PhutilXHPASTBinary' => 'parser/xhpast/bin/PhutilXHPASTBinary.php',
'PhutilXHPASTSyntaxHighlighter' => 'markup/syntax/highlighter/PhutilXHPASTSyntaxHighlighter.php',
'PhutilXHPASTSyntaxHighlighterFuture' => 'markup/syntax/highlighter/xhpast/PhutilXHPASTSyntaxHighlighterFuture.php',
'PhutilXHPASTSyntaxHighlighterTestCase' => 'markup/syntax/highlighter/__tests__/PhutilXHPASTSyntaxHighlighterTestCase.php',
'QueryFuture' => 'future/query/QueryFuture.php',
'TempFile' => 'filesystem/TempFile.php',
'TestAbstractDirectedGraph' => 'utils/__tests__/TestAbstractDirectedGraph.php',
'XHPASTNode' => 'parser/xhpast/api/XHPASTNode.php',
'XHPASTNodeTestCase' => 'parser/xhpast/api/__tests__/XHPASTNodeTestCase.php',
'XHPASTSyntaxErrorException' => 'parser/xhpast/api/XHPASTSyntaxErrorException.php',
'XHPASTToken' => 'parser/xhpast/api/XHPASTToken.php',
'XHPASTTree' => 'parser/xhpast/api/XHPASTTree.php',
'XHPASTTreeTestCase' => 'parser/xhpast/api/__tests__/XHPASTTreeTestCase.php',
'XsprintfUnknownConversionException' => 'xsprintf/exception/XsprintfUnknownConversionException.php',
),
'function' => array(
'array_fuse' => 'utils/utils.php',
'array_interleave' => 'utils/utils.php',
'array_mergev' => 'utils/utils.php',
'array_select_keys' => 'utils/utils.php',
'assert_instances_of' => 'utils/utils.php',
'assert_stringlike' => 'utils/utils.php',
'coalesce' => 'utils/utils.php',
'csprintf' => 'xsprintf/csprintf.php',
'exec_manual' => 'future/exec/execx.php',
'execx' => 'future/exec/execx.php',
'head' => 'utils/utils.php',
'head_key' => 'utils/utils.php',
'hgsprintf' => 'xsprintf/hgsprintf.php',
'hsprintf' => 'markup/render.php',
'id' => 'utils/utils.php',
'idx' => 'utils/utils.php',
'idxv' => 'utils/utils.php',
'ifilter' => 'utils/utils.php',
'igroup' => 'utils/utils.php',
'ipull' => 'utils/utils.php',
'isort' => 'utils/utils.php',
'jsprintf' => 'xsprintf/jsprintf.php',
'last' => 'utils/utils.php',
'last_key' => 'utils/utils.php',
'ldap_sprintf' => 'xsprintf/ldapsprintf.php',
'mfilter' => 'utils/utils.php',
'mgroup' => 'utils/utils.php',
'mpull' => 'utils/utils.php',
'msort' => 'utils/utils.php',
'msortv' => 'utils/utils.php',
'newv' => 'utils/utils.php',
'nonempty' => 'utils/utils.php',
'phlog' => 'error/phlog.php',
'pht' => 'internationalization/pht.php',
'phutil_censor_credentials' => 'utils/utils.php',
'phutil_console_confirm' => 'console/format.php',
'phutil_console_format' => 'console/format.php',
'phutil_console_get_terminal_width' => 'console/format.php',
'phutil_console_prompt' => 'console/format.php',
'phutil_console_require_tty' => 'console/format.php',
'phutil_console_select' => 'console/format.php',
'phutil_console_wrap' => 'console/format.php',
'phutil_count' => 'internationalization/pht.php',
'phutil_date_format' => 'utils/viewutils.php',
'phutil_deprecated' => 'moduleutils/moduleutils.php',
'phutil_error_listener_example' => 'error/phlog.php',
'phutil_escape_html' => 'markup/render.php',
'phutil_escape_html_newlines' => 'markup/render.php',
'phutil_escape_uri' => 'markup/render.php',
'phutil_escape_uri_path_component' => 'markup/render.php',
'phutil_fnmatch' => 'utils/utils.php',
'phutil_format_bytes' => 'utils/viewutils.php',
'phutil_format_relative_time' => 'utils/viewutils.php',
'phutil_format_relative_time_detailed' => 'utils/viewutils.php',
'phutil_format_units_generic' => 'utils/viewutils.php',
'phutil_fwrite_nonblocking_stream' => 'utils/utils.php',
'phutil_get_current_library_name' => 'moduleutils/moduleutils.php',
'phutil_get_library_name_for_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root' => 'moduleutils/moduleutils.php',
'phutil_get_library_root_for_path' => 'moduleutils/moduleutils.php',
'phutil_get_signal_name' => 'future/exec/execx.php',
'phutil_hashes_are_identical' => 'utils/utils.php',
'phutil_implode_html' => 'markup/render.php',
'phutil_ini_decode' => 'utils/utils.php',
'phutil_is_hiphop_runtime' => 'utils/utils.php',
'phutil_is_utf8' => 'utils/utf8.php',
'phutil_is_utf8_slowly' => 'utils/utf8.php',
'phutil_is_utf8_with_only_bmp_characters' => 'utils/utf8.php',
'phutil_is_windows' => 'utils/utils.php',
'phutil_json_decode' => 'utils/utils.php',
'phutil_json_encode' => 'utils/utils.php',
'phutil_load_library' => 'moduleutils/core.php',
'phutil_loggable_string' => 'utils/utils.php',
'phutil_parse_bytes' => 'utils/viewutils.php',
'phutil_passthru' => 'future/exec/execx.php',
'phutil_person' => 'internationalization/pht.php',
'phutil_register_library' => 'moduleutils/core.php',
'phutil_register_library_map' => 'moduleutils/core.php',
'phutil_safe_html' => 'markup/render.php',
'phutil_split_lines' => 'utils/utils.php',
'phutil_tag' => 'markup/render.php',
'phutil_tag_div' => 'markup/render.php',
'phutil_unescape_uri_path_component' => 'markup/render.php',
'phutil_units' => 'utils/utils.php',
'phutil_utf8_console_strlen' => 'utils/utf8.php',
'phutil_utf8_convert' => 'utils/utf8.php',
'phutil_utf8_encode_codepoint' => 'utils/utf8.php',
'phutil_utf8_hard_wrap' => 'utils/utf8.php',
'phutil_utf8_hard_wrap_html' => 'utils/utf8.php',
'phutil_utf8_is_combining_character' => 'utils/utf8.php',
'phutil_utf8_strlen' => 'utils/utf8.php',
'phutil_utf8_strtolower' => 'utils/utf8.php',
'phutil_utf8_strtoupper' => 'utils/utf8.php',
'phutil_utf8_strtr' => 'utils/utf8.php',
'phutil_utf8_ucwords' => 'utils/utf8.php',
'phutil_utf8ize' => 'utils/utf8.php',
'phutil_utf8v' => 'utils/utf8.php',
'phutil_utf8v_codepoints' => 'utils/utf8.php',
'phutil_utf8v_combine_characters' => 'utils/utf8.php',
'phutil_utf8v_combined' => 'utils/utf8.php',
'phutil_validate_json' => 'utils/utils.php',
'phutil_var_export' => 'utils/utils.php',
'ppull' => 'utils/utils.php',
'pregsprintf' => 'xsprintf/pregsprintf.php',
'qsprintf' => 'xsprintf/qsprintf.php',
'qsprintf_check_scalar_type' => 'xsprintf/qsprintf.php',
'qsprintf_check_type' => 'xsprintf/qsprintf.php',
'queryfx' => 'xsprintf/queryfx.php',
'queryfx_all' => 'xsprintf/queryfx.php',
'queryfx_one' => 'xsprintf/queryfx.php',
'tsprintf' => 'xsprintf/tsprintf.php',
'urisprintf' => 'xsprintf/urisprintf.php',
'vcsprintf' => 'xsprintf/csprintf.php',
'vjsprintf' => 'xsprintf/jsprintf.php',
'vqsprintf' => 'xsprintf/qsprintf.php',
'vurisprintf' => 'xsprintf/urisprintf.php',
'xhp_parser_node_constants' => 'parser/xhpast/parser_nodes.php',
'xhpast_parser_token_constants' => 'parser/xhpast/parser_tokens.php',
'xsprintf' => 'xsprintf/xsprintf.php',
'xsprintf_callback_example' => 'xsprintf/xsprintf.php',
'xsprintf_command' => 'xsprintf/csprintf.php',
'xsprintf_javascript' => 'xsprintf/jsprintf.php',
'xsprintf_ldap' => 'xsprintf/ldapsprintf.php',
'xsprintf_mercurial' => 'xsprintf/hgsprintf.php',
'xsprintf_query' => 'xsprintf/qsprintf.php',
'xsprintf_regex' => 'xsprintf/pregsprintf.php',
'xsprintf_terminal' => 'xsprintf/tsprintf.php',
'xsprintf_uri' => 'xsprintf/urisprintf.php',
),
'xmap' => array(
'AASTNode' => 'Phobject',
'AASTNodeList' => array(
'Phobject',
'Countable',
'Iterator',
),
'AASTToken' => 'Phobject',
'AASTTree' => 'Phobject',
'AbstractDirectedGraph' => 'Phobject',
'AbstractDirectedGraphTestCase' => 'PhutilTestCase',
'AphrontAccessDeniedQueryException' => 'AphrontQueryException',
'AphrontBaseMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontCharacterSetQueryException' => 'AphrontQueryException',
'AphrontConnectionLostQueryException' => 'AphrontRecoverableQueryException',
'AphrontConnectionQueryException' => 'AphrontQueryException',
'AphrontCountQueryException' => 'AphrontQueryException',
'AphrontDatabaseConnection' => array(
'Phobject',
'PhutilQsprintfInterface',
),
'AphrontDatabaseTransactionState' => 'Phobject',
'AphrontDeadlockQueryException' => 'AphrontRecoverableQueryException',
'AphrontDuplicateKeyQueryException' => 'AphrontQueryException',
'AphrontInvalidCredentialsQueryException' => 'AphrontQueryException',
'AphrontIsolatedDatabaseConnection' => 'AphrontDatabaseConnection',
'AphrontLockTimeoutQueryException' => 'AphrontRecoverableQueryException',
'AphrontMySQLDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
'AphrontMySQLiDatabaseConnection' => 'AphrontBaseMySQLDatabaseConnection',
'AphrontNotSupportedQueryException' => 'AphrontQueryException',
'AphrontObjectMissingQueryException' => 'AphrontQueryException',
'AphrontParameterQueryException' => 'AphrontQueryException',
'AphrontQueryException' => 'Exception',
'AphrontQueryTimeoutQueryException' => 'AphrontRecoverableQueryException',
'AphrontRecoverableQueryException' => 'AphrontQueryException',
'AphrontRequestStream' => 'Phobject',
'AphrontSchemaQueryException' => 'AphrontQueryException',
'AphrontScopedUnguardedWriteCapability' => 'Phobject',
'AphrontWriteGuard' => 'Phobject',
'BaseHTTPFuture' => 'Future',
'CaseInsensitiveArray' => 'PhutilArray',
'CaseInsensitiveArrayTestCase' => 'PhutilTestCase',
'CommandException' => 'Exception',
'ConduitClient' => 'Phobject',
'ConduitClientException' => 'Exception',
'ConduitClientTestCase' => 'PhutilTestCase',
'ConduitFuture' => 'FutureProxy',
'ExecFuture' => 'PhutilExecutableFuture',
'ExecFutureTestCase' => 'PhutilTestCase',
'ExecPassthruTestCase' => 'PhutilTestCase',
'FileFinder' => 'Phobject',
'FileFinderTestCase' => 'PhutilTestCase',
'FileList' => 'Phobject',
'Filesystem' => 'Phobject',
'FilesystemException' => 'Exception',
'FilesystemTestCase' => 'PhutilTestCase',
'Future' => 'Phobject',
'FutureIterator' => array(
'Phobject',
'Iterator',
),
'FutureIteratorTestCase' => 'PhutilTestCase',
'FutureProxy' => 'Future',
'HTTPFuture' => 'BaseHTTPFuture',
'HTTPFutureCURLResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureCertificateResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureHTTPResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureParseResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPFutureResponseStatus' => 'Exception',
'HTTPFutureTransportResponseStatus' => 'HTTPFutureResponseStatus',
'HTTPSFuture' => 'BaseHTTPFuture',
'ImmediateFuture' => 'Future',
'LibphutilUSEnglishTranslation' => 'PhutilTranslation',
'LinesOfALarge' => array(
'Phobject',
'Iterator',
),
'LinesOfALargeExecFuture' => 'LinesOfALarge',
'LinesOfALargeExecFutureTestCase' => 'PhutilTestCase',
'LinesOfALargeFile' => 'LinesOfALarge',
'LinesOfALargeFileTestCase' => 'PhutilTestCase',
'MFilterTestHelper' => 'Phobject',
'PHPASTParserTestCase' => 'PhutilTestCase',
'PhageAction' => 'Phobject',
'PhageAgentAction' => 'PhageAction',
'PhageAgentBootloader' => 'Phobject',
'PhageAgentTestCase' => 'PhutilTestCase',
'PhageExecuteAction' => 'PhageAction',
'PhageLocalAction' => 'PhageAgentAction',
'PhagePHPAgent' => 'Phobject',
'PhagePHPAgentBootloader' => 'PhageAgentBootloader',
'PhagePlanAction' => 'PhageAction',
'Phobject' => 'Iterator',
'PhobjectTestCase' => 'PhutilTestCase',
'PhutilAPCKeyValueCache' => 'PhutilKeyValueCache',
'PhutilAWSEC2Future' => 'PhutilAWSFuture',
'PhutilAWSException' => 'Exception',
'PhutilAWSFuture' => 'FutureProxy',
'PhutilAWSManagementWorkflow' => 'PhutilArgumentWorkflow',
'PhutilAWSS3DeleteManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSS3Future' => 'PhutilAWSFuture',
'PhutilAWSS3GetManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSS3ManagementWorkflow' => 'PhutilAWSManagementWorkflow',
'PhutilAWSS3PutManagementWorkflow' => 'PhutilAWSS3ManagementWorkflow',
'PhutilAWSv4Signature' => 'Phobject',
'PhutilAWSv4SignatureTestCase' => 'PhutilTestCase',
'PhutilAggregateException' => 'Exception',
'PhutilAllCapsEnglishLocale' => 'PhutilLocale',
'PhutilAmazonAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilArgumentParser' => 'Phobject',
'PhutilArgumentParserException' => 'Exception',
'PhutilArgumentParserTestCase' => 'PhutilTestCase',
'PhutilArgumentSpecification' => 'Phobject',
'PhutilArgumentSpecificationException' => 'PhutilArgumentParserException',
'PhutilArgumentSpecificationTestCase' => 'PhutilTestCase',
'PhutilArgumentSpellingCorrector' => 'Phobject',
'PhutilArgumentSpellingCorrectorTestCase' => 'PhutilTestCase',
'PhutilArgumentUsageException' => 'PhutilArgumentParserException',
'PhutilArgumentWorkflow' => 'Phobject',
'PhutilArray' => array(
'Phobject',
'Countable',
'ArrayAccess',
'Iterator',
),
'PhutilArrayTestCase' => 'PhutilTestCase',
'PhutilArrayWithDefaultValue' => 'PhutilArray',
'PhutilAsanaAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilAsanaFuture' => 'FutureProxy',
'PhutilAuthAdapter' => 'Phobject',
'PhutilAuthConfigurationException' => 'PhutilAuthException',
'PhutilAuthCredentialException' => 'PhutilAuthException',
'PhutilAuthException' => 'Exception',
'PhutilAuthUserAbortedException' => 'PhutilAuthException',
'PhutilBacktraceSignalHandler' => 'PhutilSignalHandler',
'PhutilBallOfPHP' => 'Phobject',
'PhutilBitbucketAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilBootloaderException' => 'Exception',
'PhutilBritishEnglishLocale' => 'PhutilLocale',
'PhutilBufferedIterator' => array(
'Phobject',
'Iterator',
),
'PhutilBufferedIteratorTestCase' => 'PhutilTestCase',
'PhutilBugtraqParser' => 'Phobject',
'PhutilBugtraqParserTestCase' => 'PhutilTestCase',
'PhutilCIDRBlock' => 'Phobject',
'PhutilCIDRList' => 'Phobject',
'PhutilCLikeCodeSnippetContextFreeGrammar' => 'PhutilCodeSnippetContextFreeGrammar',
'PhutilCalendarAbsoluteDateTime' => 'PhutilCalendarDateTime',
'PhutilCalendarContainerNode' => 'PhutilCalendarNode',
'PhutilCalendarDateTime' => 'Phobject',
'PhutilCalendarDateTimeTestCase' => 'PhutilTestCase',
'PhutilCalendarDocumentNode' => 'PhutilCalendarContainerNode',
'PhutilCalendarDuration' => 'Phobject',
'PhutilCalendarEventNode' => 'PhutilCalendarContainerNode',
'PhutilCalendarNode' => 'Phobject',
'PhutilCalendarProxyDateTime' => 'PhutilCalendarDateTime',
'PhutilCalendarRawNode' => 'PhutilCalendarContainerNode',
'PhutilCalendarRecurrenceList' => 'PhutilCalendarRecurrenceSource',
'PhutilCalendarRecurrenceRule' => 'PhutilCalendarRecurrenceSource',
'PhutilCalendarRecurrenceRuleTestCase' => 'PhutilTestCase',
'PhutilCalendarRecurrenceSet' => 'Phobject',
'PhutilCalendarRecurrenceSource' => 'Phobject',
'PhutilCalendarRecurrenceTestCase' => 'PhutilTestCase',
'PhutilCalendarRelativeDateTime' => 'PhutilCalendarProxyDateTime',
'PhutilCalendarRootNode' => 'PhutilCalendarContainerNode',
'PhutilCalendarUserNode' => 'PhutilCalendarNode',
'PhutilCallbackFilterIterator' => 'FilterIterator',
'PhutilCallbackSignalHandler' => 'PhutilSignalHandler',
'PhutilChannel' => 'Phobject',
'PhutilChannelChannel' => 'PhutilChannel',
'PhutilChannelTestCase' => 'PhutilTestCase',
'PhutilChunkedIterator' => array(
'Phobject',
'Iterator',
),
'PhutilChunkedIteratorTestCase' => 'PhutilTestCase',
'PhutilClassMapQuery' => 'Phobject',
'PhutilCodeSnippetContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilCommandString' => 'Phobject',
'PhutilConsole' => 'Phobject',
'PhutilConsoleBlock' => 'PhutilConsoleView',
'PhutilConsoleFormatter' => 'Phobject',
'PhutilConsoleList' => 'PhutilConsoleView',
'PhutilConsoleMessage' => 'Phobject',
'PhutilConsoleProgressBar' => 'Phobject',
'PhutilConsoleServer' => 'Phobject',
'PhutilConsoleServerChannel' => 'PhutilChannelChannel',
'PhutilConsoleStdinNotInteractiveException' => 'Exception',
'PhutilConsoleSyntaxHighlighter' => 'Phobject',
'PhutilConsoleTable' => 'PhutilConsoleView',
'PhutilConsoleView' => 'Phobject',
'PhutilConsoleWrapTestCase' => 'PhutilTestCase',
'PhutilContextFreeGrammar' => 'Phobject',
'PhutilCowsay' => 'Phobject',
'PhutilCowsayTestCase' => 'PhutilTestCase',
'PhutilCsprintfTestCase' => 'PhutilTestCase',
'PhutilCzechLocale' => 'PhutilLocale',
'PhutilDaemon' => 'Phobject',
'PhutilDaemonHandle' => 'Phobject',
'PhutilDaemonOverseer' => 'Phobject',
'PhutilDaemonOverseerModule' => 'Phobject',
+ 'PhutilDaemonPool' => 'Phobject',
'PhutilDefaultSyntaxHighlighter' => 'Phobject',
'PhutilDefaultSyntaxHighlighterEngine' => 'PhutilSyntaxHighlighterEngine',
'PhutilDefaultSyntaxHighlighterEnginePygmentsFuture' => 'FutureProxy',
'PhutilDefaultSyntaxHighlighterEngineTestCase' => 'PhutilTestCase',
'PhutilDeferredLog' => 'Phobject',
'PhutilDeferredLogTestCase' => 'PhutilTestCase',
'PhutilDirectedScalarGraph' => 'AbstractDirectedGraph',
'PhutilDirectoryFixture' => 'Phobject',
'PhutilDirectoryKeyValueCache' => 'PhutilKeyValueCache',
'PhutilDisqusAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilDivinerSyntaxHighlighter' => 'Phobject',
'PhutilDocblockParser' => 'Phobject',
'PhutilDocblockParserTestCase' => 'PhutilTestCase',
'PhutilEditDistanceMatrix' => 'Phobject',
'PhutilEditDistanceMatrixTestCase' => 'PhutilTestCase',
'PhutilEditorConfig' => 'Phobject',
'PhutilEditorConfigTestCase' => 'PhutilTestCase',
'PhutilEmailAddress' => 'Phobject',
'PhutilEmailAddressTestCase' => 'PhutilTestCase',
'PhutilEmojiLocale' => 'PhutilLocale',
'PhutilEmptyAuthAdapter' => 'PhutilAuthAdapter',
'PhutilEnglishCanadaLocale' => 'PhutilLocale',
'PhutilErrorHandler' => 'Phobject',
'PhutilErrorHandlerTestCase' => 'PhutilTestCase',
'PhutilErrorTrap' => 'Phobject',
'PhutilEvent' => 'Phobject',
'PhutilEventConstants' => 'Phobject',
'PhutilEventEngine' => 'Phobject',
'PhutilEventListener' => 'Phobject',
'PhutilEventType' => 'PhutilEventConstants',
'PhutilExampleBufferedIterator' => 'PhutilBufferedIterator',
'PhutilExcessiveServiceCallsDaemon' => 'PhutilTortureTestDaemon',
'PhutilExecChannel' => 'PhutilChannel',
'PhutilExecPassthru' => 'PhutilExecutableFuture',
'PhutilExecutableFuture' => 'Future',
'PhutilExecutionEnvironment' => 'Phobject',
'PhutilExtensionsTestCase' => 'PhutilTestCase',
'PhutilFacebookAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilFatalDaemon' => 'PhutilTortureTestDaemon',
'PhutilFileLock' => 'PhutilLock',
'PhutilFileLockTestCase' => 'PhutilTestCase',
'PhutilFileTree' => 'Phobject',
'PhutilFrenchLocale' => 'PhutilLocale',
'PhutilGermanLocale' => 'PhutilLocale',
'PhutilGitHubAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilGitHubFuture' => 'FutureProxy',
'PhutilGitHubResponse' => 'Phobject',
'PhutilGitURI' => 'Phobject',
'PhutilGitURITestCase' => 'PhutilTestCase',
'PhutilGoogleAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilHTTPEngineExtension' => 'Phobject',
'PhutilHangForeverDaemon' => 'PhutilTortureTestDaemon',
'PhutilHashingIterator' => array(
'PhutilProxyIterator',
'Iterator',
),
'PhutilHashingIteratorTestCase' => 'PhutilTestCase',
'PhutilHelpArgumentWorkflow' => 'PhutilArgumentWorkflow',
'PhutilHgsprintfTestCase' => 'PhutilTestCase',
'PhutilHighIntensityIntervalDaemon' => 'PhutilTortureTestDaemon',
'PhutilICSParser' => 'Phobject',
'PhutilICSParserException' => 'Exception',
'PhutilICSParserTestCase' => 'PhutilTestCase',
'PhutilICSWriter' => 'Phobject',
'PhutilICSWriterTestCase' => 'PhutilTestCase',
'PhutilINIParserException' => 'Exception',
'PhutilIPAddress' => 'Phobject',
'PhutilIPAddressTestCase' => 'PhutilTestCase',
'PhutilIPv4Address' => 'PhutilIPAddress',
'PhutilIPv6Address' => 'PhutilIPAddress',
'PhutilInRequestKeyValueCache' => 'PhutilKeyValueCache',
'PhutilInteractiveEditor' => 'Phobject',
'PhutilInvalidRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilInvalidStateException' => 'Exception',
'PhutilInvalidStateExceptionTestCase' => 'PhutilTestCase',
'PhutilInvisibleSyntaxHighlighter' => 'Phobject',
'PhutilIrreducibleRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilJIRAAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilJSON' => 'Phobject',
'PhutilJSONFragmentLexer' => 'PhutilLexer',
'PhutilJSONFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
'PhutilJSONParser' => 'Phobject',
'PhutilJSONParserException' => 'Exception',
'PhutilJSONParserTestCase' => 'PhutilTestCase',
'PhutilJSONProtocolChannel' => 'PhutilProtocolChannel',
'PhutilJSONProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilJSONTestCase' => 'PhutilTestCase',
'PhutilJavaCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
'PhutilKeyValueCache' => 'Phobject',
'PhutilKeyValueCacheNamespace' => 'PhutilKeyValueCacheProxy',
'PhutilKeyValueCacheProfiler' => 'PhutilKeyValueCacheProxy',
'PhutilKeyValueCacheProxy' => 'PhutilKeyValueCache',
'PhutilKeyValueCacheStack' => 'PhutilKeyValueCache',
'PhutilKeyValueCacheTestCase' => 'PhutilTestCase',
'PhutilKoreanLocale' => 'PhutilLocale',
'PhutilLDAPAuthAdapter' => 'PhutilAuthAdapter',
'PhutilLanguageGuesser' => 'Phobject',
'PhutilLanguageGuesserTestCase' => 'PhutilTestCase',
'PhutilLexer' => 'Phobject',
'PhutilLexerSyntaxHighlighter' => 'PhutilSyntaxHighlighter',
'PhutilLibraryConflictException' => 'Exception',
'PhutilLibraryMapBuilder' => 'Phobject',
'PhutilLibraryTestCase' => 'PhutilTestCase',
'PhutilLipsumContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilLocale' => 'Phobject',
'PhutilLocaleTestCase' => 'PhutilTestCase',
'PhutilLock' => 'Phobject',
'PhutilLockException' => 'Exception',
'PhutilLogFileChannel' => 'PhutilChannelChannel',
'PhutilLunarPhase' => 'Phobject',
'PhutilLunarPhaseTestCase' => 'PhutilTestCase',
'PhutilMarkupEngine' => 'Phobject',
'PhutilMarkupTestCase' => 'PhutilTestCase',
'PhutilMemcacheKeyValueCache' => 'PhutilKeyValueCache',
'PhutilMethodNotImplementedException' => 'Exception',
'PhutilMetricsChannel' => 'PhutilChannelChannel',
'PhutilMissingSymbolException' => 'Exception',
'PhutilModuleUtilsTestCase' => 'PhutilTestCase',
'PhutilNiceDaemon' => 'PhutilTortureTestDaemon',
'PhutilNumber' => 'Phobject',
'PhutilOAuth1AuthAdapter' => 'PhutilAuthAdapter',
'PhutilOAuth1Future' => 'FutureProxy',
'PhutilOAuth1FutureTestCase' => 'PhutilTestCase',
'PhutilOAuthAuthAdapter' => 'PhutilAuthAdapter',
'PhutilOnDiskKeyValueCache' => 'PhutilKeyValueCache',
'PhutilOpaqueEnvelope' => 'Phobject',
'PhutilOpaqueEnvelopeKey' => 'Phobject',
'PhutilOpaqueEnvelopeTestCase' => 'PhutilTestCase',
'PhutilPHPCodeSnippetContextFreeGrammar' => 'PhutilCLikeCodeSnippetContextFreeGrammar',
'PhutilPHPFragmentLexer' => 'PhutilLexer',
'PhutilPHPFragmentLexerHighlighterTestCase' => 'PhutilTestCase',
'PhutilPHPFragmentLexerTestCase' => 'PhutilTestCase',
'PhutilPHPObjectProtocolChannel' => 'PhutilProtocolChannel',
'PhutilPHPObjectProtocolChannelTestCase' => 'PhutilTestCase',
'PhutilParserGenerator' => 'Phobject',
'PhutilParserGeneratorException' => 'Exception',
'PhutilParserGeneratorTestCase' => 'PhutilTestCase',
'PhutilPayPalAPIFuture' => 'FutureProxy',
'PhutilPersonTest' => array(
'Phobject',
'PhutilPerson',
),
'PhutilPhabricatorAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilPhtTestCase' => 'PhutilTestCase',
'PhutilPirateEnglishLocale' => 'PhutilLocale',
'PhutilPortugueseBrazilLocale' => 'PhutilLocale',
'PhutilPortuguesePortugalLocale' => 'PhutilLocale',
'PhutilPregsprintfTestCase' => 'PhutilTestCase',
'PhutilProcessGroupDaemon' => 'PhutilTortureTestDaemon',
'PhutilProseDiff' => 'Phobject',
'PhutilProseDiffTestCase' => 'PhutilTestCase',
'PhutilProseDifferenceEngine' => 'Phobject',
'PhutilProtocolChannel' => 'PhutilChannelChannel',
'PhutilProxyException' => 'Exception',
'PhutilProxyIterator' => array(
'Phobject',
'Iterator',
),
'PhutilPygmentizeParser' => 'Phobject',
'PhutilPygmentizeParserTestCase' => 'PhutilTestCase',
'PhutilPygmentsSyntaxHighlighter' => 'Phobject',
'PhutilPythonFragmentLexer' => 'PhutilLexer',
'PhutilQueryStringParser' => 'Phobject',
'PhutilQueryStringParserTestCase' => 'PhutilTestCase',
'PhutilRainbowSyntaxHighlighter' => 'Phobject',
'PhutilRawEnglishLocale' => 'PhutilLocale',
'PhutilReadableSerializer' => 'Phobject',
'PhutilReadableSerializerTestCase' => 'PhutilTestCase',
'PhutilRealNameContextFreeGrammar' => 'PhutilContextFreeGrammar',
'PhutilRemarkupBlockInterpreter' => 'Phobject',
'PhutilRemarkupBlockRule' => 'Phobject',
'PhutilRemarkupBlockStorage' => 'Phobject',
'PhutilRemarkupBoldRule' => 'PhutilRemarkupRule',
'PhutilRemarkupCodeBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupDefaultBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupDelRule' => 'PhutilRemarkupRule',
'PhutilRemarkupDocumentLinkRule' => 'PhutilRemarkupRule',
'PhutilRemarkupEngine' => 'PhutilMarkupEngine',
'PhutilRemarkupEngineTestCase' => 'PhutilTestCase',
'PhutilRemarkupEscapeRemarkupRule' => 'PhutilRemarkupRule',
'PhutilRemarkupHeaderBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupHighlightRule' => 'PhutilRemarkupRule',
'PhutilRemarkupHorizontalRuleBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupHyperlinkRule' => 'PhutilRemarkupRule',
'PhutilRemarkupInlineBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupInterpreterBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupItalicRule' => 'PhutilRemarkupRule',
'PhutilRemarkupLinebreaksRule' => 'PhutilRemarkupRule',
'PhutilRemarkupListBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupLiteralBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupMonospaceRule' => 'PhutilRemarkupRule',
'PhutilRemarkupNoteBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupQuotesBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupReplyBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupRule' => 'Phobject',
'PhutilRemarkupSimpleTableBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupTableBlockRule' => 'PhutilRemarkupBlockRule',
'PhutilRemarkupTestInterpreterRule' => 'PhutilRemarkupBlockInterpreter',
'PhutilRemarkupUnderlineRule' => 'PhutilRemarkupRule',
'PhutilRope' => 'Phobject',
'PhutilRopeTestCase' => 'PhutilTestCase',
'PhutilSafeHTML' => 'Phobject',
'PhutilSafeHTMLTestCase' => 'PhutilTestCase',
'PhutilSaturateStdoutDaemon' => 'PhutilTortureTestDaemon',
'PhutilSearchQueryCompiler' => 'Phobject',
'PhutilSearchQueryCompilerSyntaxException' => 'Exception',
'PhutilSearchQueryCompilerTestCase' => 'PhutilTestCase',
'PhutilSearchStemmer' => 'Phobject',
'PhutilSearchStemmerTestCase' => 'PhutilTestCase',
'PhutilServiceProfiler' => 'Phobject',
'PhutilShellLexer' => 'PhutilLexer',
'PhutilShellLexerTestCase' => 'PhutilTestCase',
'PhutilSignalHandler' => 'Phobject',
'PhutilSignalRouter' => 'Phobject',
'PhutilSimpleOptions' => 'Phobject',
'PhutilSimpleOptionsLexer' => 'PhutilLexer',
'PhutilSimpleOptionsLexerTestCase' => 'PhutilTestCase',
'PhutilSimpleOptionsTestCase' => 'PhutilTestCase',
'PhutilSimplifiedChineseLocale' => 'PhutilLocale',
'PhutilSlackAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilSlackFuture' => 'FutureProxy',
'PhutilSocketChannel' => 'PhutilChannel',
'PhutilSortVector' => 'Phobject',
'PhutilSpanishSpainLocale' => 'PhutilLocale',
'PhutilSprite' => 'Phobject',
'PhutilSpriteSheet' => 'Phobject',
'PhutilStreamIterator' => array(
'Phobject',
'Iterator',
),
'PhutilSyntaxHighlighter' => 'Phobject',
'PhutilSyntaxHighlighterEngine' => 'Phobject',
'PhutilSyntaxHighlighterException' => 'Exception',
'PhutilSystem' => 'Phobject',
'PhutilSystemTestCase' => 'PhutilTestCase',
'PhutilTerminalString' => 'Phobject',
'PhutilTestPhobject' => 'Phobject',
'PhutilTortureTestDaemon' => 'PhutilDaemon',
'PhutilTraditionalChineseLocale' => 'PhutilLocale',
'PhutilTranslation' => 'Phobject',
'PhutilTranslationTestCase' => 'PhutilTestCase',
'PhutilTranslator' => 'Phobject',
'PhutilTranslatorTestCase' => 'PhutilTestCase',
'PhutilTsprintfTestCase' => 'PhutilTestCase',
'PhutilTwitchAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilTwitchFuture' => 'FutureProxy',
'PhutilTwitterAuthAdapter' => 'PhutilOAuth1AuthAdapter',
'PhutilTypeCheckException' => 'Exception',
'PhutilTypeExtraParametersException' => 'Exception',
'PhutilTypeLexer' => 'PhutilLexer',
'PhutilTypeMissingParametersException' => 'Exception',
'PhutilTypeSpec' => 'Phobject',
'PhutilTypeSpecTestCase' => 'PhutilTestCase',
'PhutilURI' => 'Phobject',
'PhutilURITestCase' => 'PhutilTestCase',
'PhutilUSEnglishLocale' => 'PhutilLocale',
'PhutilUTF8StringTruncator' => 'Phobject',
'PhutilUTF8TestCase' => 'PhutilTestCase',
'PhutilUnknownSymbolParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableRuleParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUnreachableTerminalParserGeneratorException' => 'PhutilParserGeneratorException',
'PhutilUrisprintfTestCase' => 'PhutilTestCase',
'PhutilUtilsTestCase' => 'PhutilTestCase',
'PhutilVeryWowEnglishLocale' => 'PhutilLocale',
'PhutilWordPressAuthAdapter' => 'PhutilOAuthAuthAdapter',
'PhutilWordPressFuture' => 'FutureProxy',
'PhutilXHPASTBinary' => 'Phobject',
'PhutilXHPASTSyntaxHighlighter' => 'Phobject',
'PhutilXHPASTSyntaxHighlighterFuture' => 'FutureProxy',
'PhutilXHPASTSyntaxHighlighterTestCase' => 'PhutilTestCase',
'QueryFuture' => 'Future',
'TempFile' => 'Phobject',
'TestAbstractDirectedGraph' => 'AbstractDirectedGraph',
'XHPASTNode' => 'AASTNode',
'XHPASTNodeTestCase' => 'PhutilTestCase',
'XHPASTSyntaxErrorException' => 'Exception',
'XHPASTToken' => 'AASTToken',
'XHPASTTree' => 'AASTTree',
'XHPASTTreeTestCase' => 'PhutilTestCase',
'XsprintfUnknownConversionException' => 'InvalidArgumentException',
),
));
diff --git a/src/daemon/PhutilDaemon.php b/src/daemon/PhutilDaemon.php
index 629a567..1cc0df8 100644
--- a/src/daemon/PhutilDaemon.php
+++ b/src/daemon/PhutilDaemon.php
@@ -1,407 +1,353 @@
<?php
/**
* Scaffolding for implementing robust background processing scripts.
*
*
* Autoscaling
* ===========
*
* Autoscaling automatically launches copies of a daemon when it is busy
* (scaling the pool up) and stops them when they're idle (scaling the pool
* down). This is appropriate for daemons which perform highly parallelizable
* work.
*
* To make a daemon support autoscaling, the implementation should look
* something like this:
*
* while (!$this->shouldExit()) {
* if (work_available()) {
* $this->willBeginWork();
* do_work();
* $this->sleep(0);
* } else {
* $this->willBeginIdle();
* $this->sleep(1);
* }
* }
*
* In particular, call @{method:willBeginWork} before becoming busy, and
* @{method:willBeginIdle} when no work is available. If the daemon is launched
* into an autoscale pool, this will cause the pool to automatically scale up
* when busy and down when idle.
*
* See @{class:PhutilHighIntensityIntervalDaemon} for an example of a simple
* autoscaling daemon.
*
* Launching a daemon which does not make these callbacks into an autoscale
* pool will have no effect.
*
* @task overseer Communicating With the Overseer
* @task autoscale Autoscaling Daemon Pools
*/
abstract class PhutilDaemon extends Phobject {
const MESSAGETYPE_STDOUT = 'stdout';
const MESSAGETYPE_HEARTBEAT = 'heartbeat';
const MESSAGETYPE_BUSY = 'busy';
const MESSAGETYPE_IDLE = 'idle';
const MESSAGETYPE_DOWN = 'down';
const WORKSTATE_BUSY = 'busy';
const WORKSTATE_IDLE = 'idle';
private $argv;
private $traceMode;
private $traceMemory;
private $verbose;
private $notifyReceived;
private $inGracefulShutdown;
private $workState = null;
private $idleSince = null;
- private $autoscaleProperties = array();
+ private $scaledownDuration;
final public function setVerbose($verbose) {
$this->verbose = $verbose;
return $this;
}
final public function getVerbose() {
return $this->verbose;
}
+ final public function setScaledownDuration($scaledown_duration) {
+ $this->scaledownDuration = $scaledown_duration;
+ return $this;
+ }
+
+ final public function getScaledownDuration() {
+ return $this->scaledownDuration;
+ }
+
final public function __construct(array $argv) {
$this->argv = $argv;
$router = PhutilSignalRouter::getRouter();
$handler_key = 'daemon.term';
if (!$router->getHandler($handler_key)) {
$handler = new PhutilCallbackSignalHandler(
SIGTERM,
__CLASS__.'::onTermSignal');
$router->installHandler($handler_key, $handler);
}
pcntl_signal(SIGINT, array($this, 'onGracefulSignal'));
pcntl_signal(SIGUSR2, array($this, 'onNotifySignal'));
// Without discard mode, this consumes unbounded amounts of memory. Keep
// memory bounded.
PhutilServiceProfiler::getInstance()->enableDiscardMode();
$this->beginStdoutCapture();
}
final public function __destruct() {
$this->endStdoutCapture();
}
final public function stillWorking() {
$this->emitOverseerMessage(self::MESSAGETYPE_HEARTBEAT, null);
if ($this->traceMemory) {
$daemon = get_class($this);
fprintf(
STDERR,
"%s %s %s\n",
'<RAMS>',
$daemon,
pht(
'Memory Usage: %s KB',
new PhutilNumber(memory_get_usage() / 1024, 1)));
}
}
final public function shouldExit() {
return $this->inGracefulShutdown;
}
final protected function sleep($duration) {
$this->notifyReceived = false;
$this->willSleep($duration);
$this->stillWorking();
- $is_autoscale = $this->isClonedAutoscaleDaemon();
- $scale_down = $this->getAutoscaleDownDuration();
+ $scale_down = $this->getScaledownDuration();
$max_sleep = 60;
- if ($is_autoscale) {
+ if ($scale_down) {
$max_sleep = min($max_sleep, $scale_down);
}
- if ($is_autoscale) {
+ if ($scale_down) {
if ($this->workState == self::WORKSTATE_IDLE) {
$dur = (time() - $this->idleSince);
$this->log(pht('Idle for %s seconds.', $dur));
}
}
while ($duration > 0 &&
!$this->notifyReceived &&
!$this->shouldExit()) {
// If this is an autoscaling clone and we've been idle for too long,
// we're going to scale the pool down by exiting and not restarting. The
// DOWN message tells the overseer that we don't want to be restarted.
- if ($is_autoscale) {
+ if ($scale_down) {
if ($this->workState == self::WORKSTATE_IDLE) {
if ($this->idleSince && ($this->idleSince + $scale_down < time())) {
$this->inGracefulShutdown = true;
$this->emitOverseerMessage(self::MESSAGETYPE_DOWN, null);
$this->log(
pht(
'Daemon was idle for more than %s second(s), '.
'scaling pool down.',
new PhutilNumber($scale_down)));
break;
}
}
}
sleep(min($duration, $max_sleep));
$duration -= $max_sleep;
$this->stillWorking();
}
}
protected function willSleep($duration) {
return;
}
public static function onTermSignal($signo) {
self::didCatchSignal($signo);
}
final protected function getArgv() {
return $this->argv;
}
final public function execute() {
$this->willRun();
$this->run();
}
abstract protected function run();
final public function setTraceMemory() {
$this->traceMemory = true;
return $this;
}
final public function getTraceMemory() {
return $this->traceMemory;
}
final public function setTraceMode() {
$this->traceMode = true;
PhutilServiceProfiler::installEchoListener();
PhutilConsole::getConsole()->getServer()->setEnableLog(true);
$this->didSetTraceMode();
return $this;
}
final public function getTraceMode() {
return $this->traceMode;
}
final public function onGracefulSignal($signo) {
self::didCatchSignal($signo);
$this->inGracefulShutdown = true;
}
final public function onNotifySignal($signo) {
self::didCatchSignal($signo);
$this->notifyReceived = true;
$this->onNotify($signo);
}
protected function onNotify($signo) {
// This is a hook for subclasses.
}
protected function willRun() {
// This is a hook for subclasses.
}
protected function didSetTraceMode() {
// This is a hook for subclasses.
}
final protected function log($message) {
if ($this->verbose) {
$daemon = get_class($this);
fprintf(STDERR, "%s %s %s\n", '<VERB>', $daemon, $message);
}
}
private static function didCatchSignal($signo) {
$signame = phutil_get_signal_name($signo);
fprintf(
STDERR,
"%s Caught signal %s (%s).\n",
'<SGNL>',
$signo,
$signame);
}
/* -( Communicating With the Overseer )------------------------------------ */
private function beginStdoutCapture() {
ob_start(array($this, 'didReceiveStdout'), 2);
}
private function endStdoutCapture() {
ob_end_flush();
}
public function didReceiveStdout($data) {
if (!strlen($data)) {
return '';
}
return $this->encodeOverseerMessage(self::MESSAGETYPE_STDOUT, $data);
}
private function encodeOverseerMessage($type, $data) {
$structure = array($type);
if ($data !== null) {
$structure[] = $data;
}
return json_encode($structure)."\n";
}
private function emitOverseerMessage($type, $data) {
$this->endStdoutCapture();
echo $this->encodeOverseerMessage($type, $data);
$this->beginStdoutCapture();
}
public static function errorListener($event, $value, array $metadata) {
// If the caller has redirected the error log to a file, PHP won't output
// messages to stderr, so the overseer can't capture them. Install a
// listener which just echoes errors to stderr, so the overseer is always
// aware of errors.
$console = PhutilConsole::getConsole();
$message = idx($metadata, 'default_message');
if ($message) {
$console->writeErr("%s\n", $message);
}
if (idx($metadata, 'trace')) {
$trace = PhutilErrorHandler::formatStacktrace($metadata['trace']);
$console->writeErr("%s\n", $trace);
}
}
/* -( Autoscaling )-------------------------------------------------------- */
/**
* Prepare to become busy. This may autoscale the pool up.
*
* This notifies the overseer that the daemon has become busy. If daemons
* that are part of an autoscale pool are continuously busy for a prolonged
* period of time, the overseer may scale up the pool.
*
* @return this
* @task autoscale
*/
protected function willBeginWork() {
if ($this->workState != self::WORKSTATE_BUSY) {
$this->workState = self::WORKSTATE_BUSY;
$this->idleSince = null;
$this->emitOverseerMessage(self::MESSAGETYPE_BUSY, null);
}
return $this;
}
/**
* Prepare to idle. This may autoscale the pool down.
*
* This notifies the overseer that the daemon is no longer busy. If daemons
- * that are part of an autoscale pool are idle for a prolonged period of time,
- * they may exit to scale the pool down.
+ * that are part of an autoscale pool are idle for a prolonged period of
+ * time, they may exit to scale the pool down.
*
* @return this
* @task autoscale
*/
protected function willBeginIdle() {
if ($this->workState != self::WORKSTATE_IDLE) {
$this->workState = self::WORKSTATE_IDLE;
$this->idleSince = time();
$this->emitOverseerMessage(self::MESSAGETYPE_IDLE, null);
}
return $this;
}
-
-
- /**
- * Determine if this is a clone or the original daemon.
- *
- * @return bool True if this is an cloned autoscaling daemon.
- * @task autoscale
- */
- private function isClonedAutoscaleDaemon() {
- return (bool)$this->getAutoscaleProperty('clone', false);
- }
-
-
- /**
- * Get the duration (in seconds) which a daemon must be continuously idle
- * for before it should exit to scale the pool down.
- *
- * @return int Duration, in seconds.
- * @task autoscale
- */
- private function getAutoscaleDownDuration() {
- return $this->getAutoscaleProperty('down', 15);
- }
-
-
- /**
- * Configure autoscaling for this daemon.
- *
- * @param map<string, wild> Map of autoscale properties.
- * @return this
- * @task autoscale
- */
- public function setAutoscaleProperties(array $autoscale_properties) {
- PhutilTypeSpec::checkMap(
- $autoscale_properties,
- array(
- 'group' => 'optional string',
- 'up' => 'optional int',
- 'down' => 'optional int',
- 'pool' => 'optional int',
- 'clone' => 'optional bool',
- 'reserve' => 'optional int|float',
- ));
-
- $this->autoscaleProperties = $autoscale_properties;
-
- return $this;
- }
-
-
- /**
- * Read autoscaling configuration for this daemon.
- *
- * @param string Property to read.
- * @param wild Default value to return if the property is not set.
- * @return wild Property value, or `$default` if one is not set.
- * @task autoscale
- */
- private function getAutoscaleProperty($key, $default = null) {
- return idx($this->autoscaleProperties, $key, $default);
- }
-
}
diff --git a/src/daemon/PhutilDaemonHandle.php b/src/daemon/PhutilDaemonHandle.php
index e6ff223..c81256d 100644
--- a/src/daemon/PhutilDaemonHandle.php
+++ b/src/daemon/PhutilDaemonHandle.php
@@ -1,409 +1,458 @@
<?php
final class PhutilDaemonHandle extends Phobject {
const EVENT_DID_LAUNCH = 'daemon.didLaunch';
const EVENT_DID_LOG = 'daemon.didLogMessage';
const EVENT_DID_HEARTBEAT = 'daemon.didHeartbeat';
const EVENT_WILL_GRACEFUL = 'daemon.willGraceful';
const EVENT_WILL_EXIT = 'daemon.willExit';
- private $overseer;
- private $daemonClass;
+ private $pool;
+ private $properties;
+ private $future;
private $argv;
- private $config;
+
+ private $restartAt;
+ private $busyEpoch;
+
private $pid;
private $daemonID;
private $deadline;
private $heartbeat;
private $stdoutBuffer;
- private $restartAt;
private $shouldRestart = true;
private $shouldShutdown;
- private $future;
- private $traceMemory;
-
- public function __construct(
- PhutilDaemonOverseer $overseer,
- $daemon_class,
- array $argv,
- array $config) {
-
- $this->overseer = $overseer;
- $this->daemonClass = $daemon_class;
- $this->argv = $argv;
- $this->config = $config;
+
+ private function __construct() {
+ // <empty>
+ }
+
+ public static function newFromConfig(array $config) {
+ PhutilTypeSpec::checkMap(
+ $config,
+ array(
+ 'class' => 'string',
+ 'argv' => 'optional list<string>',
+ 'load' => 'optional list<string>',
+ 'log' => 'optional string|null',
+ 'down' => 'optional int',
+ ));
+
+ $config = $config + array(
+ 'argv' => array(),
+ 'load' => array(),
+ 'log' => null,
+ 'down' => 15,
+ );
+
+ $daemon = new self();
+ $daemon->properties = $config;
+ $daemon->daemonID = $daemon->generateDaemonID();
+
+ return $daemon;
+ }
+
+ public function setDaemonPool(PhutilDaemonPool $daemon_pool) {
+ $this->pool = $daemon_pool;
+ return $this;
+ }
+
+ public function getDaemonPool() {
+ return $this->pool;
+ }
+
+ public function getBusyEpoch() {
+ return $this->busyEpoch;
+ }
+
+ public function getDaemonClass() {
+ return $this->getProperty('class');
+ }
+
+ private function getProperty($key) {
+ return idx($this->properties, $key);
+ }
+
+ public function setCommandLineArguments(array $arguments) {
+ $this->argv = $arguments;
+ return $this;
+ }
+
+ public function getCommandLineArguments() {
+ return $this->argv;
+ }
+
+ public function getDaemonArguments() {
+ return $this->getProperty('argv');
+ }
+
+ public function didLaunch() {
$this->restartAt = time();
- $this->daemonID = $this->generateDaemonID();
$this->dispatchEvent(
self::EVENT_DID_LAUNCH,
array(
- 'argv' => $this->argv,
- 'explicitArgv' => idx($this->config, 'argv'),
+ 'argv' => $this->getCommandLineArguments(),
+ 'explicitArgv' => $this->getDaemonArguments(),
));
+
+ return $this;
}
public function isRunning() {
return (bool)$this->future;
}
public function isDone() {
return (!$this->shouldRestart && !$this->isRunning());
}
public function getFuture() {
return $this->future;
}
- public function setTraceMemory($trace_memory) {
- $this->traceMemory = $trace_memory;
- return $this;
- }
-
- public function getTraceMemory() {
- return $this->traceMemory;
- }
-
public function update() {
- $this->updateMemory();
-
if (!$this->isRunning()) {
if (!$this->shouldRestart) {
return;
}
if (!$this->restartAt || (time() < $this->restartAt)) {
return;
}
if ($this->shouldShutdown) {
return;
}
$this->startDaemonProcess();
}
$future = $this->future;
$result = null;
if ($future->isReady()) {
$result = $future->resolve();
}
list($stdout, $stderr) = $future->read();
$future->discardBuffers();
if (strlen($stdout)) {
$this->didReadStdout($stdout);
}
$stderr = trim($stderr);
if (strlen($stderr)) {
foreach (phutil_split_lines($stderr, false) as $line) {
$this->logMessage('STDE', $line);
}
}
if ($result !== null) {
list($err) = $result;
if ($err) {
$this->logMessage('FAIL', pht('Process exited with error %s.', $err));
} else {
$this->logMessage('DONE', pht('Process exited normally.'));
}
$this->future = null;
if ($this->shouldShutdown) {
$this->restartAt = null;
+ $this->dispatchEvent(self::EVENT_WILL_EXIT);
} else {
$this->scheduleRestart();
}
}
$this->updateHeartbeatEvent();
$this->updateHangDetection();
}
private function updateHeartbeatEvent() {
if ($this->heartbeat > time()) {
return;
}
$this->heartbeat = time() + $this->getHeartbeatEventFrequency();
$this->dispatchEvent(self::EVENT_DID_HEARTBEAT);
}
private function updateHangDetection() {
if (!$this->isRunning()) {
return;
}
if (time() > $this->deadline) {
$this->logMessage('HANG', pht('Hang detected. Restarting process.'));
$this->annihilateProcessGroup();
$this->scheduleRestart();
}
}
private function scheduleRestart() {
$this->logMessage('WAIT', pht('Waiting to restart process.'));
$this->restartAt = time() + self::getWaitBeforeRestart();
}
/**
* Generate a unique ID for this daemon.
*
* @return string A unique daemon ID.
*/
private function generateDaemonID() {
return substr(getmypid().':'.Filesystem::readRandomCharacters(12), 0, 12);
}
public function getDaemonID() {
return $this->daemonID;
}
public function getPID() {
return $this->pid;
}
private function getCaptureBufferSize() {
return 65535;
}
private function getRequiredHeartbeatFrequency() {
return 86400;
}
public static function getWaitBeforeRestart() {
return 5;
}
public static function getHeartbeatEventFrequency() {
return 120;
}
private function getKillDelay() {
return 3;
}
private function getDaemonCWD() {
$root = dirname(phutil_get_library_root('phutil'));
return $root.'/scripts/daemon/exec/';
}
private function newExecFuture() {
- $class = $this->daemonClass;
- $argv = $this->argv;
+ $class = $this->getDaemonClass();
+ $argv = $this->getCommandLineArguments();
$buffer_size = $this->getCaptureBufferSize();
// NOTE: PHP implements proc_open() by running 'sh -c'. On most systems this
// is bash, but on Ubuntu it's dash. When you proc_open() using bash, you
// get one new process (the command you ran). When you proc_open() using
// dash, you get two new processes: the command you ran and a parent
// "dash -c" (or "sh -c") process. This means that the child process's PID
// is actually the 'dash' PID, not the command's PID. To avoid this, use
// 'exec' to replace the shell process with the real process; without this,
// the child will call posix_getppid(), be given the pid of the 'sh -c'
// process, and send it SIGUSR1 to keepalive which will terminate it
// immediately. We also won't be able to do process group management because
// the shell process won't properly posix_setsid() so the pgid of the child
// won't be meaningful.
+ $config = $this->properties;
+ unset($config['class']);
+ $config = phutil_json_encode($config);
+
return id(new ExecFuture('exec ./exec_daemon.php %s %Ls', $class, $argv))
->setCWD($this->getDaemonCWD())
->setStdoutSizeLimit($buffer_size)
->setStderrSizeLimit($buffer_size)
- ->write(json_encode($this->config));
+ ->write($config);
}
/**
* Dispatch an event to event listeners.
*
* @param string Event type.
* @param dict Event parameters.
* @return void
*/
private function dispatchEvent($type, array $params = array()) {
$data = array(
- 'id' => $this->daemonID,
- 'daemonClass' => $this->daemonClass,
- 'childPID' => $this->pid,
+ 'id' => $this->getDaemonID(),
+ 'daemonClass' => $this->getDaemonClass(),
+ 'childPID' => $this->getPID(),
) + $params;
$event = new PhutilEvent($type, $data);
try {
PhutilEventEngine::dispatchEvent($event);
} catch (Exception $ex) {
phlog($ex);
}
}
private function annihilateProcessGroup() {
- $pid = $this->pid;
+ $pid = $this->getPID();
+
$pgid = posix_getpgid($pid);
if ($pid && $pgid) {
posix_kill(-$pgid, SIGTERM);
sleep($this->getKillDelay());
posix_kill(-$pgid, SIGKILL);
$this->pid = null;
}
}
- private function updateMemory() {
- if ($this->traceMemory) {
- $this->logMessage(
- 'RAMS',
- pht(
- 'Overseer Memory Usage: %s KB',
- new PhutilNumber(memory_get_usage() / 1024, 1)));
- }
- }
-
private function startDaemonProcess() {
$this->logMessage('INIT', pht('Starting process.'));
$this->deadline = time() + $this->getRequiredHeartbeatFrequency();
$this->heartbeat = time() + self::getHeartbeatEventFrequency();
$this->stdoutBuffer = '';
$this->future = $this->newExecFuture();
$this->future->start();
$this->pid = $this->future->getPID();
}
private function didReadStdout($data) {
$this->stdoutBuffer .= $data;
while (true) {
$pos = strpos($this->stdoutBuffer, "\n");
if ($pos === false) {
break;
}
$message = substr($this->stdoutBuffer, 0, $pos);
$this->stdoutBuffer = substr($this->stdoutBuffer, $pos + 1);
try {
$structure = phutil_json_decode($message);
} catch (PhutilJSONParserException $ex) {
$structure = array();
}
switch (idx($structure, 0)) {
case PhutilDaemon::MESSAGETYPE_STDOUT:
$this->logMessage('STDO', idx($structure, 1));
break;
case PhutilDaemon::MESSAGETYPE_HEARTBEAT:
$this->deadline = time() + $this->getRequiredHeartbeatFrequency();
break;
case PhutilDaemon::MESSAGETYPE_BUSY:
- $this->overseer->didBeginWork($this);
+ if (!$this->busyEpoch) {
+ $this->busyEpoch = time();
+ }
break;
case PhutilDaemon::MESSAGETYPE_IDLE:
- $this->overseer->didBeginIdle($this);
+ $this->busyEpoch = null;
break;
case PhutilDaemon::MESSAGETYPE_DOWN:
// The daemon is exiting because it doesn't have enough work and it
// is trying to scale the pool down. We should not restart it.
$this->shouldRestart = false;
$this->shouldShutdown = true;
break;
default:
// If we can't parse this or it isn't a message we understand, just
// emit the raw message.
$this->logMessage('STDO', pht('<Malformed> %s', $message));
break;
}
}
}
public function didReceiveNotifySignal($signo) {
- $pid = $this->pid;
+ $pid = $this->getPID();
if ($pid) {
posix_kill($pid, $signo);
}
}
public function didReceiveReloadSignal($signo) {
$signame = phutil_get_signal_name($signo);
if ($signame) {
$sigmsg = pht(
'Reloading in response to signal %d (%s).',
$signo,
$signame);
} else {
$sigmsg = pht(
'Reloading in response to signal %d.',
$signo);
}
$this->logMessage('RELO', $sigmsg, $signo);
// This signal means "stop the current process gracefully, then launch
// a new identical process once it exits". This can be used to update
// daemons after code changes (the new processes will run the new code)
// without aborting any running tasks.
// We SIGINT the daemon but don't set the shutdown flag, so it will
// naturally be restarted after it exits, as though it had exited after an
// unhandled exception.
- posix_kill($this->pid, SIGINT);
+ posix_kill($this->getPID(), SIGINT);
}
public function didReceiveGracefulSignal($signo) {
$this->shouldShutdown = true;
$this->shouldRestart = false;
$signame = phutil_get_signal_name($signo);
if ($signame) {
$sigmsg = pht(
'Graceful shutdown in response to signal %d (%s).',
$signo,
$signame);
} else {
$sigmsg = pht(
'Graceful shutdown in response to signal %d.',
$signo);
}
$this->logMessage('DONE', $sigmsg, $signo);
- posix_kill($this->pid, SIGINT);
+ posix_kill($this->getPID(), SIGINT);
}
- public function didReceiveTerminalSignal($signo) {
+ public function didReceiveTerminateSignal($signo) {
$this->shouldShutdown = true;
$this->shouldRestart = false;
$signame = phutil_get_signal_name($signo);
if ($signame) {
$sigmsg = pht(
'Shutting down in response to signal %s (%s).',
$signo,
$signame);
} else {
$sigmsg = pht('Shutting down in response to signal %s.', $signo);
}
$this->logMessage('EXIT', $sigmsg, $signo);
$this->annihilateProcessGroup();
}
private function logMessage($type, $message, $context = null) {
- $this->overseer->logMessage($type, $message, $context);
+ $this->getDaemonPool()->logMessage($type, $message, $context);
+
$this->dispatchEvent(
self::EVENT_DID_LOG,
array(
'type' => $type,
'message' => $message,
'context' => $context,
));
}
- public function didRemoveDaemon() {
- $this->dispatchEvent(self::EVENT_WILL_EXIT);
+ public function toDictionary() {
+ return array(
+ 'pid' => $this->getPID(),
+ 'id' => $this->getDaemonID(),
+ 'config' => $this->properties,
+ );
}
+
}
diff --git a/src/daemon/PhutilDaemonOverseer.php b/src/daemon/PhutilDaemonOverseer.php
index 14d9b3f..fc5bbc6 100644
--- a/src/daemon/PhutilDaemonOverseer.php
+++ b/src/daemon/PhutilDaemonOverseer.php
@@ -1,536 +1,453 @@
<?php
/**
* Oversees a daemon and restarts it if it fails.
+ *
+ * @task signals Signal Handling
*/
final class PhutilDaemonOverseer extends Phobject {
private $argv;
- private $moreArgs;
- private $inAbruptShutdown;
- private $inGracefulShutdown;
private static $instance;
private $config;
- private $daemons = array();
+ private $pools = array();
private $traceMode;
private $traceMemory;
private $daemonize;
private $piddir;
private $log;
private $libraries = array();
private $modules = array();
private $verbose;
- private $err = 0;
private $lastPidfile;
private $startEpoch;
private $autoscale = array();
private $autoscaleConfig = array();
+ const SIGNAL_NOTIFY = 'signal/notify';
+ const SIGNAL_RELOAD = 'signal/reload';
+ const SIGNAL_GRACEFUL = 'signal/graceful';
+ const SIGNAL_TERMINATE = 'signal/terminate';
+
+ private $err = 0;
+ private $inAbruptShutdown;
+ private $inGracefulShutdown;
+
public function __construct(array $argv) {
PhutilServiceProfiler::getInstance()->enableDiscardMode();
$args = new PhutilArgumentParser($argv);
$args->setTagline(pht('daemon overseer'));
$args->setSynopsis(<<<EOHELP
**launch_daemon.php** [__options__] __daemon__
Launch and oversee an instance of __daemon__.
EOHELP
);
$args->parseStandardArguments();
$args->parse(
array(
array(
'name' => 'trace-memory',
'help' => pht('Enable debug memory tracing.'),
),
array(
'name' => 'verbose',
'help' => pht('Enable verbose activity logging.'),
),
array(
'name' => 'label',
'short' => 'l',
'param' => 'label',
'help' => pht(
'Optional process label. Makes "%s" nicer, no behavioral effects.',
'ps'),
),
));
$argv = array();
if ($args->getArg('trace')) {
$this->traceMode = true;
$argv[] = '--trace';
}
if ($args->getArg('trace-memory')) {
$this->traceMode = true;
$this->traceMemory = true;
$argv[] = '--trace-memory';
}
$verbose = $args->getArg('verbose');
if ($verbose) {
$this->verbose = true;
$argv[] = '--verbose';
}
$label = $args->getArg('label');
if ($label) {
$argv[] = '-l';
$argv[] = $label;
}
$this->argv = $argv;
if (function_exists('posix_isatty') && posix_isatty(STDIN)) {
fprintf(STDERR, pht('Reading daemon configuration from stdin...')."\n");
}
$config = @file_get_contents('php://stdin');
$config = id(new PhutilJSONParser())->parse($config);
$this->libraries = idx($config, 'load');
$this->log = idx($config, 'log');
$this->daemonize = idx($config, 'daemonize');
$this->piddir = idx($config, 'piddir');
$this->config = $config;
if (self::$instance) {
throw new Exception(
pht('You may not instantiate more than one Overseer per process.'));
}
self::$instance = $this;
$this->startEpoch = time();
// Check this before we daemonize, since if it's an issue the child will
// exit immediately.
if ($this->piddir) {
$dir = $this->piddir;
try {
Filesystem::assertWritable($dir);
} catch (Exception $ex) {
throw new Exception(
pht(
"Specified daemon PID directory ('%s') does not exist or is ".
"not writable by the daemon user!",
$dir));
}
}
if (!idx($config, 'daemons')) {
throw new PhutilArgumentUsageException(
pht('You must specify at least one daemon to start!'));
}
if ($this->log) {
// NOTE: Now that we're committed to daemonizing, redirect the error
// log if we have a `--log` parameter. Do this at the last moment
// so as many setup issues as possible are surfaced.
ini_set('error_log', $this->log);
}
if ($this->daemonize) {
// We need to get rid of these or the daemon will hang when we TERM it
// waiting for something to read the buffers. TODO: Learn how unix works.
fclose(STDOUT);
fclose(STDERR);
ob_start();
$pid = pcntl_fork();
if ($pid === -1) {
throw new Exception(pht('Unable to fork!'));
} else if ($pid) {
exit(0);
}
}
$this->modules = PhutilDaemonOverseerModule::getAllModules();
- pcntl_signal(SIGUSR2, array($this, 'didReceiveNotifySignal'));
-
- pcntl_signal(SIGHUP, array($this, 'didReceiveReloadSignal'));
- pcntl_signal(SIGINT, array($this, 'didReceiveGracefulSignal'));
- pcntl_signal(SIGTERM, array($this, 'didReceiveTerminalSignal'));
+ $this->installSignalHandlers();
}
public function addLibrary($library) {
$this->libraries[] = $library;
return $this;
}
public function run() {
- $this->daemons = array();
-
- foreach ($this->config['daemons'] as $config) {
- $config += array(
- 'argv' => array(),
- 'autoscale' => array(),
- );
-
- $daemon = new PhutilDaemonHandle(
- $this,
- $config['class'],
- $this->argv,
- array(
- 'log' => $this->log,
- 'argv' => $config['argv'],
- 'load' => $this->libraries,
- 'autoscale' => $config['autoscale'],
- ));
-
- $daemon->setTraceMemory($this->traceMemory);
-
- $this->addDaemon($daemon, $config);
-
- $group = idx($config['autoscale'], 'group');
- if (strlen($group)) {
- if (isset($this->autoscaleConfig[$group])) {
- throw new Exception(
- pht(
- 'Two daemons are part of the same autoscale group ("%s"). '.
- 'Each daemon autoscale group must be unique.',
- $group));
- }
- $this->autoscaleConfig[$group] = $config;
- }
- }
-
- $should_reload = false;
+ $this->createDaemonPools();
while (true) {
- foreach ($this->modules as $module) {
- try {
- if ($module->shouldReloadDaemons()) {
- $this->logMessage(
- 'RELO',
- pht(
- 'Reloading daemons (triggered by overseer module "%s").',
- get_class($module)));
- $should_reload = true;
- }
- } catch (Exception $ex) {
- phlog($ex);
- }
- }
-
- if ($should_reload) {
- $this->didReceiveReloadSignal(SIGHUP);
- $should_reload = false;
+ if ($this->shouldReloadDaemons()) {
+ $this->didReceiveSignal(SIGHUP);
}
$futures = array();
- foreach ($this->getDaemonHandles() as $daemon) {
- $daemon->update();
- if ($daemon->isRunning()) {
- $futures[] = $daemon->getFuture();
- }
+ foreach ($this->getDaemonPools() as $pool) {
+ $pool->updatePool();
- if ($daemon->isDone()) {
- $this->removeDaemon($daemon);
+ foreach ($pool->getFutures() as $future) {
+ $futures[] = $future;
}
}
$this->updatePidfile();
- $this->updateAutoscale();
+ $this->updateMemory();
- if ($futures) {
- $iter = id(new FutureIterator($futures))
- ->setUpdateInterval(1);
- foreach ($iter as $future) {
- break;
- }
- } else {
+ $this->waitForDaemonFutures($futures);
+
+ if (!$futures) {
if ($this->inGracefulShutdown) {
break;
}
- sleep(1);
}
}
exit($this->err);
}
- private function addDaemon(PhutilDaemonHandle $daemon, array $config) {
- $id = $daemon->getDaemonID();
- $this->daemons[$id] = array(
- 'handle' => $daemon,
- 'config' => $config,
- );
- $autoscale_group = $this->getAutoscaleGroup($daemon);
- if ($autoscale_group) {
- $this->autoscale[$autoscale_group][$id] = true;
- }
-
- return $this;
- }
-
- private function removeDaemon(PhutilDaemonHandle $daemon) {
- $id = $daemon->getDaemonID();
-
- $autoscale_group = $this->getAutoscaleGroup($daemon);
- if ($autoscale_group) {
- unset($this->autoscale[$autoscale_group][$id]);
- }
+ private function waitForDaemonFutures(array $futures) {
+ assert_instances_of($futures, 'ExecFuture');
- unset($this->daemons[$id]);
-
- $daemon->didRemoveDaemon();
-
- return $this;
- }
-
- private function getAutoscaleGroup(PhutilDaemonHandle $daemon) {
- $id = $daemon->getDaemonID();
- $autoscale = $this->daemons[$id]['config']['autoscale'];
- return idx($autoscale, 'group');
- }
-
- private function getAutoscaleProperty($group_key, $key, $default = null) {
- $config = $this->autoscaleConfig[$group_key]['autoscale'];
- return idx($config, $key, $default);
- }
-
- public function didBeginWork(PhutilDaemonHandle $daemon) {
- $id = $daemon->getDaemonID();
- $busy = idx($this->daemons[$daemon->getDaemonID()], 'busy');
- if (!$busy) {
- $this->daemons[$id]['busy'] = time();
- }
- }
-
- public function didBeginIdle(PhutilDaemonHandle $daemon) {
- $id = $daemon->getDaemonID();
- unset($this->daemons[$id]['busy']);
- }
-
- public function updateAutoscale() {
- if ($this->inGracefulShutdown) {
- return;
- }
-
- foreach ($this->autoscale as $group => $daemons) {
- $scaleup_duration = $this->getAutoscaleProperty($group, 'up', 2);
- $max_pool_size = $this->getAutoscaleProperty($group, 'pool', 8);
- $reserve = $this->getAutoscaleProperty($group, 'reserve', 0);
-
- // Don't scale a group if it is already at the maximum pool size.
- if (count($daemons) >= $max_pool_size) {
- continue;
+ if ($futures) {
+ // TODO: This only wakes if any daemons actually exit. It would be a bit
+ // cleaner to wait on any I/O with Channels.
+ $iter = id(new FutureIterator($futures))
+ ->setUpdateInterval(1);
+ foreach ($iter as $future) {
+ break;
}
-
- $should_scale = true;
- foreach ($daemons as $daemon_id => $ignored) {
- $busy = idx($this->daemons[$daemon_id], 'busy');
- if (!$busy) {
- // At least one daemon in the group hasn't reported that it has
- // started work.
- $should_scale = false;
- break;
- }
-
- if ((time() - $busy) < $scaleup_duration) {
- // At least one daemon in the group was idle recently, so we have
- // not fullly
- $should_scale = false;
- break;
- }
- }
-
- // If we have a configured memory reserve for this pool, it tells us that
- // we should not scale up unless there's at least that much memory left
- // on the system (for example, a reserve of 0.25 means that 25% of system
- // memory must be free to autoscale).
- if ($should_scale && $reserve) {
- // On some systems this may be slightly more expensive than other
- // checks, so only do it once we're prepared to scale up.
- $memory = PhutilSystem::getSystemMemoryInformation();
- $free_ratio = ($memory['free'] / $memory['total']);
-
- // If we don't have enough free memory, don't scale.
- if ($free_ratio <= $reserve) {
- continue;
- }
- }
-
- if ($should_scale) {
- $config = $this->autoscaleConfig[$group];
-
- $config['autoscale']['clone'] = true;
-
- $clone = new PhutilDaemonHandle(
- $this,
- $config['class'],
- $this->argv,
- array(
- 'log' => $this->log,
- 'argv' => $config['argv'],
- 'load' => $this->libraries,
- 'autoscale' => $config['autoscale'],
- ));
-
- $this->logMessage(
- 'AUTO',
- pht(
- 'Scaling pool "%s" up to %s daemon(s).',
- $group,
- new PhutilNumber(count($daemons) + 1)));
-
- $this->addDaemon($clone, $config);
-
- // Don't scale more than one pool up per iteration. Otherwise, we could
- // break the memory barrier if we have a lot of pools and scale them
- // all up at once.
- return;
+ } else {
+ if (!$this->inGracefulShutdown) {
+ sleep(1);
}
}
}
- public function didReceiveNotifySignal($signo) {
- foreach ($this->getDaemonHandles() as $daemon) {
- $daemon->didReceiveNotifySignal($signo);
- }
- }
+ private function createDaemonPools() {
+ $configs = $this->config['daemons'];
- public function didReceiveReloadSignal($signo) {
- foreach ($this->getDaemonHandles() as $daemon) {
- $daemon->didReceiveReloadSignal($signo);
- }
- }
+ $forced_options = array(
+ 'load' => $this->libraries,
+ 'log' => $this->log,
+ );
- public function didReceiveGracefulSignal($signo) {
- // If we receive SIGINT more than once, interpret it like SIGTERM.
- if ($this->inGracefulShutdown) {
- return $this->didReceiveTerminalSignal($signo);
- }
- $this->inGracefulShutdown = true;
+ foreach ($configs as $config) {
+ $config = $forced_options + $config;
- foreach ($this->getDaemonHandles() as $daemon) {
- $daemon->didReceiveGracefulSignal($signo);
- }
- }
+ $pool = PhutilDaemonPool::newFromConfig($config)
+ ->setOverseer($this)
+ ->setCommandLineArguments($this->argv);
- public function didReceiveTerminalSignal($signo) {
- $this->err = 128 + $signo;
- if ($this->inAbruptShutdown) {
- exit($this->err);
- }
- $this->inAbruptShutdown = true;
-
- foreach ($this->getDaemonHandles() as $daemon) {
- $daemon->didReceiveTerminalSignal($signo);
+ $this->pools[] = $pool;
}
}
- private function getDaemonHandles() {
- return ipull($this->daemons, 'handle');
+ private function getDaemonPools() {
+ return $this->pools;
}
+
/**
* Identify running daemons by examining the process table. This isn't
* completely reliable, but can be used as a fallback if the pid files fail
* or we end up with stray daemons by other means.
*
* Example output (array keys are process IDs):
*
* array(
* 12345 => array(
* 'type' => 'overseer',
* 'command' => 'php launch_daemon.php --daemonize ...',
* 'pid' => 12345,
* ),
* 12346 => array(
* 'type' => 'daemon',
* 'command' => 'php exec_daemon.php ...',
* 'pid' => 12346,
* ),
* );
*
* @return dict Map of PIDs to process information, identifying running
* daemon processes.
*/
public static function findRunningDaemons() {
$results = array();
list($err, $processes) = exec_manual('ps -o pid,command -a -x -w -w -w');
if ($err) {
return $results;
}
$processes = array_filter(explode("\n", trim($processes)));
foreach ($processes as $process) {
list($pid, $command) = preg_split('/\s+/', trim($process), 2);
$pattern = '/((launch|exec)_daemon.php|phd-daemon)/';
$matches = null;
if (!preg_match($pattern, $command, $matches)) {
continue;
}
switch ($matches[1]) {
case 'exec_daemon.php':
$type = 'daemon';
break;
case 'launch_daemon.php':
case 'phd-daemon':
default:
$type = 'overseer';
break;
}
$results[(int)$pid] = array(
'type' => $type,
'command' => $command,
'pid' => (int)$pid,
);
}
return $results;
}
private function updatePidfile() {
if (!$this->piddir) {
return;
}
- $daemons = array();
+ $pidfile = $this->toDictionary();
- foreach ($this->daemons as $daemon) {
- $handle = $daemon['handle'];
- $config = $daemon['config'];
+ if ($pidfile !== $this->lastPidfile) {
+ $this->lastPidfile = $pidfile;
+ $pidfile_path = $this->piddir.'/daemon.'.getmypid();
+ Filesystem::writeFile($pidfile_path, phutil_json_encode($pidfile));
+ }
+ }
- if (!$handle->isRunning()) {
- continue;
- }
+ public function toDictionary() {
+ $daemons = array();
+ foreach ($this->getDaemonPools() as $pool) {
+ foreach ($pool->getDaemons() as $daemon) {
+ if (!$daemon->isRunning()) {
+ continue;
+ }
- $daemons[] = array(
- 'pid' => $handle->getPID(),
- 'id' => $handle->getDaemonID(),
- 'config' => $config,
- );
+ $daemons[] = $daemon->toDictionary();
+ }
}
- $pidfile = array(
+ return array(
'pid' => getmypid(),
'start' => $this->startEpoch,
'config' => $this->config,
'daemons' => $daemons,
);
+ }
- if ($pidfile !== $this->lastPidfile) {
- $this->lastPidfile = $pidfile;
- $pidfile_path = $this->piddir.'/daemon.'.getmypid();
- Filesystem::writeFile($pidfile_path, json_encode($pidfile));
+ private function updateMemory() {
+ if (!$this->traceMemory) {
+ return;
}
+
+ $this->logMessage(
+ 'RAMS',
+ pht(
+ 'Overseer Memory Usage: %s KB',
+ new PhutilNumber(memory_get_usage() / 1024, 1)));
}
public function logMessage($type, $message, $context = null) {
if ($this->traceMode || $this->verbose) {
error_log(date('Y-m-d g:i:s A').' ['.$type.'] '.$message);
}
}
+
+/* -( Signal Handling )---------------------------------------------------- */
+
+
+ /**
+ * @task signals
+ */
+ private function installSignalHandlers() {
+ $signals = array(
+ SIGUSR2,
+ SIGHUP,
+ SIGINT,
+ SIGTERM,
+ );
+
+ foreach ($signals as $signal) {
+ pcntl_signal($signal, array($this, 'didReceiveSignal'));
+ }
+ }
+
+
+ /**
+ * @task signals
+ */
+ public function didReceiveSignal($signo) {
+ switch ($signo) {
+ case SIGUSR2:
+ $signal_type = self::SIGNAL_NOTIFY;
+ break;
+ case SIGHUP:
+ $signal_type = self::SIGNAL_RELOAD;
+ break;
+ case SIGINT:
+ // If we receive SIGINT more than once, interpret it like SIGTERM.
+ if ($this->inGracefulShutdown) {
+ return $this->didReceiveSignal(SIGTERM);
+ }
+
+ $this->inGracefulShutdown = true;
+ $signal_type = self::SIGNAL_GRACEFUL;
+ break;
+ case SIGTERM:
+ // If we receive SIGTERM more than once, terminate abruptly.
+ $this->err = 128 + $signo;
+ if ($this->inAbruptShutdown) {
+ exit($this->err);
+ }
+
+ $this->inAbruptShutdown = true;
+ $signal_type = self::SIGNAL_TERMINATE;
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Signal handler called with unknown signal type ("%d")!',
+ $signo));
+ }
+
+ foreach ($this->getDaemonPools() as $pool) {
+ $pool->didReceiveSignal($signal_type, $signo);
+ }
+ }
+
+
+/* -( Daemon Modules )----------------------------------------------------- */
+
+
+ private function getModules() {
+ return $this->modules;
+ }
+
+ private function shouldReloadDaemons() {
+ $modules = $this->getModules();
+
+ $should_reload = false;
+ foreach ($modules as $module) {
+ try {
+ // NOTE: Even if one module tells us to reload, we call the method on
+ // each module anyway to make calls a little more predictable.
+
+ if ($module->shouldReloadDaemons()) {
+ $this->logMessage(
+ 'RELO',
+ pht(
+ 'Reloading daemons (triggered by overseer module "%s").',
+ get_class($module)));
+ $should_reload = true;
+ }
+ } catch (Exception $ex) {
+ phlog($ex);
+ }
+ }
+
+ return $should_reload;
+ }
+
+
}
diff --git a/src/daemon/PhutilDaemonPool.php b/src/daemon/PhutilDaemonPool.php
new file mode 100644
index 0000000..e6080c5
--- /dev/null
+++ b/src/daemon/PhutilDaemonPool.php
@@ -0,0 +1,245 @@
+<?php
+
+final class PhutilDaemonPool extends Phobject {
+
+ private $properties = array();
+ private $commandLineArguments;
+
+ private $overseer;
+ private $daemons = array();
+ private $argv;
+
+ private function __construct() {
+ // <empty>
+ }
+
+ public static function newFromConfig(array $config) {
+ PhutilTypeSpec::checkMap(
+ $config,
+ array(
+ 'class' => 'string',
+ 'label' => 'string',
+ 'argv' => 'optional list<string>',
+ 'load' => 'optional list<string>',
+ 'log' => 'optional string|null',
+ 'pool' => 'optional int',
+ 'up' => 'optional int',
+ 'down' => 'optional int',
+ 'reserve' => 'optional int|float',
+ ));
+
+ $config = $config + array(
+ 'argv' => array(),
+ 'load' => array(),
+ 'log' => null,
+ 'pool' => 1,
+ 'up' => 2,
+ 'down' => 15,
+ 'reserve' => 0,
+ );
+
+ $pool = new self();
+ $pool->properties = $config;
+
+ return $pool;
+ }
+
+ public function setOverseer(PhutilDaemonOverseer $overseer) {
+ $this->overseer = $overseer;
+ return $this;
+ }
+
+ public function getOverseer() {
+ return $this->overseer;
+ }
+
+ public function setCommandLineArguments(array $arguments) {
+ $this->commandLineArguments = $arguments;
+ return $this;
+ }
+
+ public function getCommandLineArguments() {
+ return $this->commandLineArguments;
+ }
+
+ private function newDaemon() {
+ $config = $this->properties;
+
+ if (count($this->daemons)) {
+ $down_duration = $this->getPoolScaledownDuration();
+ } else {
+ // TODO: For now, never scale pools down to 0.
+ $down_duration = 0;
+ }
+
+ $forced_config = array(
+ 'down' => $down_duration,
+ );
+
+ $config = $forced_config + $config;
+
+ $config = array_select_keys(
+ $config,
+ array(
+ 'class',
+ 'log',
+ 'load',
+ 'argv',
+ 'down',
+ ));
+
+ $daemon = PhutilDaemonHandle::newFromConfig($config)
+ ->setDaemonPool($this)
+ ->setCommandLineArguments($this->getCommandLineArguments());
+
+ $daemon_id = $daemon->getDaemonID();
+ $this->daemons[$daemon_id] = $daemon;
+
+ $daemon->didLaunch();
+
+ return $daemon;
+ }
+
+ public function getDaemons() {
+ return $this->daemons;
+ }
+
+ public function getFutures() {
+ $futures = array();
+ foreach ($this->getDaemons() as $daemon) {
+ $future = $daemon->getFuture();
+ if ($future) {
+ $futures[] = $future;
+ }
+ }
+
+ return $futures;
+ }
+
+ public function didReceiveSignal($signal, $signo) {
+ foreach ($this->getDaemons() as $daemon) {
+ switch ($signal) {
+ case PhutilDaemonOverseer::SIGNAL_NOTIFY:
+ $daemon->didReceiveNotifySignal($signo);
+ break;
+ case PhutilDaemonOverseer::SIGNAL_RELOAD:
+ $daemon->didReceiveReloadSignal($signo);
+ break;
+ case PhutilDaemonOverseer::SIGNAL_GRACEFUL:
+ $daemon->didReceiveGracefulSignal($signo);
+ break;
+ case PhutilDaemonOverseer::SIGNAL_TERMINATE:
+ $daemon->didReceiveTerminateSignal($signo);
+ break;
+ default:
+ throw new Exception(
+ pht(
+ 'Unknown signal "%s" ("%d").',
+ $signal,
+ $signo));
+ }
+ }
+ }
+
+ public function getPoolLabel() {
+ return $this->getPoolProperty('label');
+ }
+
+ public function getPoolMaximumSize() {
+ return $this->getPoolProperty('pool');
+ }
+
+ public function getPoolScaleupDuration() {
+ return $this->getPoolProperty('up');
+ }
+
+ public function getPoolScaledownDuration() {
+ return $this->getPoolProperty('down');
+ }
+
+ public function getPoolMemoryReserve() {
+ return $this->getPoolProperty('reserve');
+ }
+
+ public function getPoolDaemonClass() {
+ return $this->getPoolProperty('class');
+ }
+
+ private function getPoolProperty($key) {
+ return idx($this->properties, $key);
+ }
+
+ public function updatePool() {
+ $daemons = $this->getDaemons();
+
+ foreach ($daemons as $key => $daemon) {
+ $daemon->update();
+
+ if ($daemon->isDone()) {
+ unset($daemons[$key]);
+ }
+ }
+
+ $this->updateAutoscale();
+ }
+
+ private function updateAutoscale() {
+ $daemons = $this->getDaemons();
+
+ // If this pool is already at the maximum size, we can't launch any new
+ // daemons.
+ $max_size = $this->getPoolMaximumSize();
+ if (count($daemons) >= $max_size) {
+ return;
+ }
+
+ $now = time();
+ $scaleup_duration = $this->getPoolScaleupDuration();
+
+ foreach ($daemons as $daemon) {
+ $busy_epoch = $daemon->getBusyEpoch();
+ // If any daemons haven't started work yet, don't scale the pool up.
+ if (!$busy_epoch) {
+ return;
+ }
+
+ // If any daemons started work very recently, wait a little while
+ // to scale the pool up.
+ $busy_for = ($now - $busy_epoch);
+ if ($busy_for < $scaleup_duration) {
+ return;
+ }
+ }
+
+ // If we have a configured memory reserve for this pool, it tells us that
+ // we should not scale up unless there's at least that much memory left
+ // on the system (for example, a reserve of 0.25 means that 25% of system
+ // memory must be free to autoscale).
+ $reserve = $this->getPoolMemoryReserve();
+ if ($reserve) {
+ // On some systems this may be slightly more expensive than other checks,
+ // so we only do it once we're prepared to scale up.
+ $memory = PhutilSystem::getSystemMemoryInformation();
+ $free_ratio = ($memory['free'] / $memory['total']);
+
+ // If we don't have enough free memory, don't scale.
+ if ($free_ratio <= $reserve) {
+ return;
+ }
+ }
+
+ $this->logMessage(
+ 'AUTO',
+ pht(
+ 'Scaling pool "%s" up to %s daemon(s).',
+ $this->getPoolLabel(),
+ new PhutilNumber(count($daemons) + 1)));
+
+ $this->newDaemon();
+ }
+
+ public function logMessage($type, $message, $context = null) {
+ return $this->getOverseer()->logMessage($type, $message, $context);
+ }
+
+}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 26, 6:39 PM (14 h, 53 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
78/28/10c420e9e07cae1599db84f36242

Event Timeline