Skip to content

Commit f93671d

Browse files
authored
Merge pull request #10 from vrza/config-process
Run ConfigurationSource in a separate process
2 parents 2aa2fbb + f0d3ceb commit f93671d

15 files changed

Lines changed: 456 additions & 158 deletions

README.adoc

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Currently LRPM is not intended to run as PID 1, nor was it tested in this role.
4343
== Usage
4444

4545
1. Implement the `ConfigurationSource`. This interface exposes a public method `loadConfiguration()`, that returns an associative array containing the configuration for all the workers (see details below).
46-
2. Implement the `Worker`. This interface exposes two public methods, `start()` and `cycle()`. The child process will call `start()`, passing the worker configuration as an argument, and then enter an endless loop, where it calls `cycle()`, and then checks for a shutdown condition (usually, the death of the LRPM supervisor process). Backoff should be implemented by the cycle() method -- be careful not to run a tight loop at times when cycles are not doing actual work!
46+
2. Implement the `Worker`. This interface exposes two public methods, `start()` and `cycle()`. The child process will call `start()`, passing the worker configuration as an argument, and then enter an endless loop, where it calls `cycle()`, and then checks for a shutdown condition (usually, the termination of the LRPM supervisor process). Backoff should be implemented at the end of the `cycle()` method, as we dispatch signal handlers right after `cycle()` returns. Be careful not to run a tight loop at times when cycles are not doing actual work!
4747

4848
=== Job configuration
4949

@@ -110,17 +110,15 @@ Default value: 10
110110

111111
=== Signal handling
112112

113-
LRPM supervisor process installs signal handlers for SIGCHLD, SIGTERM and SIGINT.
113+
LRPM supervisor process installs signal handlers for SIGCHLD, SIGTERM, SIGINT, SIGHUP and SIGUSR1.
114114

115-
You can implement and install your own signal handlers inside your Worker implementation, but make sure that your Worker process shuts down cleanly after receiving SIGTERM, otherwise LRPM will consider it unresponsive and follow up with a SIGKILL.
115+
Worker process installs default signal handlers for SIGTERM and SIGINT. Signal handlers are dispatched between loop cycles, and these default handlers will terminate the Worker.
116116

117-
Default SIGTERM and SIGINT handlers will terminate the Worker before the next loop cycle.
117+
You can implement and install your own signal handlers inside your Worker implementation, but make sure that your Worker process shuts down cleanly after receiving SIGTERM, otherwise the LRPM supervisor will consider it unresponsive and follow up with a SIGKILL.
118118

119119
=== Misc caveats
120120

121-
Be aware that code in your `ConfigurationSource` class will run in the supervisor (parent) process, while your `Worker` classes will run in child processes.
122-
123-
Sharing open sockets between parent and children through `fork(2)` is not safe. Worker processes should connect to wherever they need to connect to only after they have been spawned. For example, opening sockets in `ConfigurationSource`, in the parent process, passing them via references in the configuration associative array, and then using them in Worker processes is inherently unsafe.
121+
Be aware that code in your entry point will run in the supervisor (parent) process, while your `Worker` classes will run in child processes. The entry point should do no more than set up the autoloader and run the `ProcessManager`. Sharing open sockets between parent and children through `fork(2)` is not safe. Worker processes should connect to wherever they need to connect to only after they have been spawned.
124122

125123
== Operating LRPM
126124

@@ -146,11 +144,11 @@ opcache.file_cache_only=0
146144

147145
PHP-LRPM keeps metadata in an associative array. For efficient lookups by PID, a separate index is maintained.
148146

149-
This functionality was offloaded to a generic library https://github.com/vrza/array-with-secondary-keys[Array with Secondary Keys], that wraps a hash map and maintains secondary indexes (similar to how secondary keys in an SQL database work). Implementing this particular collection lead to creation of https://github.com/vrza/cardinal-collections[Cardinal Collections], a PHP toolkit for building collections.
147+
This functionality was offloaded to a generic library https://github.com/vrza/array-with-secondary-keys[Array with Secondary Keys], that wraps a hash map and maintains secondary indexes (similar to how secondary keys in an SQL database work). Implementing this particular collection lead to the creation of https://github.com/vrza/cardinal-collections[Cardinal Collections], a PHP toolkit for building collections.
150148

151149
==== Implement receiving, handling and responding to control messages
152150

153-
Included is the `lrpmctl` tool, which uses the https://github.com/vrza/php-tipc[tipc] library to exchange messages with a running instance of LRPM. Some examples of messages include getting the `status` of running processes (see screenshot above), and requesting a `restart` of a process.
151+
Included is the `lrpmctl` tool, which uses the https://github.com/vrza/php-tipc[tipc] library to exchange messages with a running instance of LRPM over a Unix domain socket connection. Some examples of messages include getting the `status` of all workers (see screenshot above), and requesting a `restart` of a worker process.
154152

155153
==== Make sure unresponsive processes get terminated
156154

@@ -160,6 +158,10 @@ Wait for children to terminate after sending SIGTERM, follow up with SIGKILL if
160158

161159
Implemented blocking shutdown loop that makes sure all children are terminated on shutdown, including processes that may be unresponsive.
162160

161+
==== Configuration process
162+
163+
Made `ConfigurationSource` run in a process separate from the supervisor. This is to prevent `Worker` processes inheriting sockets opened by `ConfigurationSource` code (e.g. persistent database connections). The supervisor process and the config process are using the tipc library to exchange messages over a Unix domain socket connection.
164+
163165
== Some name ideas that were considered
164166

165167
* Palermo

README.in.adoc

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Currently LRPM is not intended to run as PID 1, nor was it tested in this role.
4343
== Usage
4444

4545
1. Implement the `ConfigurationSource`. This interface exposes a public method `loadConfiguration()`, that returns an associative array containing the configuration for all the workers (see details below).
46-
2. Implement the `Worker`. This interface exposes two public methods, `start()` and `cycle()`. The child process will call `start()`, passing the worker configuration as an argument, and then enter an endless loop, where it calls `cycle()`, and then checks for a shutdown condition (usually, the death of the LRPM supervisor process). Backoff should be implemented by the cycle() method -- be careful not to run a tight loop at times when cycles are not doing actual work!
46+
2. Implement the `Worker`. This interface exposes two public methods, `start()` and `cycle()`. The child process will call `start()`, passing the worker configuration as an argument, and then enter an endless loop, where it calls `cycle()`, and then checks for a shutdown condition (usually, the termination of the LRPM supervisor process). Backoff should be implemented at the end of the `cycle()` method, as we dispatch signal handlers right after `cycle()` returns. Be careful not to run a tight loop at times when cycles are not doing actual work!
4747

4848
=== Job configuration
4949

@@ -73,17 +73,15 @@ See `example.php` for a full running example with more details.
7373

7474
=== Signal handling
7575

76-
LRPM supervisor process installs signal handlers for SIGCHLD, SIGTERM and SIGINT.
76+
LRPM supervisor process installs signal handlers for SIGCHLD, SIGTERM, SIGINT, SIGHUP and SIGUSR1.
7777

78-
You can implement and install your own signal handlers inside your Worker implementation, but make sure that your Worker process shuts down cleanly after receiving SIGTERM, otherwise LRPM will consider it unresponsive and follow up with a SIGKILL.
78+
Worker process installs default signal handlers for SIGTERM and SIGINT. Signal handlers are dispatched between loop cycles, and these default handlers will terminate the Worker.
7979

80-
Default SIGTERM and SIGINT handlers will terminate the Worker before the next loop cycle.
80+
You can implement and install your own signal handlers inside your Worker implementation, but make sure that your Worker process shuts down cleanly after receiving SIGTERM, otherwise the LRPM supervisor will consider it unresponsive and follow up with a SIGKILL.
8181

8282
=== Misc caveats
8383

84-
Be aware that code in your `ConfigurationSource` class will run in the supervisor (parent) process, while your `Worker` classes will run in child processes.
85-
86-
Sharing open sockets between parent and children through `fork(2)` is not safe. Worker processes should connect to wherever they need to connect to only after they have been spawned. For example, opening sockets in `ConfigurationSource`, in the parent process, passing them via references in the configuration associative array, and then using them in Worker processes is inherently unsafe.
84+
Be aware that code in your entry point will run in the supervisor (parent) process, while your `Worker` classes will run in child processes. The entry point should do no more than set up the autoloader and run the `ProcessManager`. Sharing open sockets between parent and children through `fork(2)` is not safe. Worker processes should connect to wherever they need to connect to only after they have been spawned.
8785

8886
== Operating LRPM
8987

@@ -109,11 +107,11 @@ opcache.file_cache_only=0
109107

110108
PHP-LRPM keeps metadata in an associative array. For efficient lookups by PID, a separate index is maintained.
111109

112-
This functionality was offloaded to a generic library https://github.com/vrza/array-with-secondary-keys[Array with Secondary Keys], that wraps a hash map and maintains secondary indexes (similar to how secondary keys in an SQL database work). Implementing this particular collection lead to creation of https://github.com/vrza/cardinal-collections[Cardinal Collections], a PHP toolkit for building collections.
110+
This functionality was offloaded to a generic library https://github.com/vrza/array-with-secondary-keys[Array with Secondary Keys], that wraps a hash map and maintains secondary indexes (similar to how secondary keys in an SQL database work). Implementing this particular collection lead to the creation of https://github.com/vrza/cardinal-collections[Cardinal Collections], a PHP toolkit for building collections.
113111

114112
==== Implement receiving, handling and responding to control messages
115113

116-
Included is the `lrpmctl` tool, which uses the https://github.com/vrza/php-tipc[tipc] library to exchange messages with a running instance of LRPM. Some examples of messages include getting the `status` of running processes (see screenshot above), and requesting a `restart` of a process.
114+
Included is the `lrpmctl` tool, which uses the https://github.com/vrza/php-tipc[tipc] library to exchange messages with a running instance of LRPM over a Unix domain socket connection. Some examples of messages include getting the `status` of all workers (see screenshot above), and requesting a `restart` of a worker process.
117115

118116
==== Make sure unresponsive processes get terminated
119117

@@ -123,6 +121,10 @@ Wait for children to terminate after sending SIGTERM, follow up with SIGKILL if
123121

124122
Implemented blocking shutdown loop that makes sure all children are terminated on shutdown, including processes that may be unresponsive.
125123

124+
==== Configuration process
125+
126+
Made `ConfigurationSource` run in a process separate from the supervisor. This is to prevent `Worker` processes inheriting sockets opened by `ConfigurationSource` code (e.g. persistent database connections). The supervisor process and the config process are using the tipc library to exchange messages over a Unix domain socket connection.
127+
126128
== Some name ideas that were considered
127129

128130
* Palermo

bin/lrpmctl

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,12 @@ probeForAutoloader($progname);
5656
use TIPC\UnixSocketStreamClient;
5757
use TextTableFormatter\Table;
5858
use TerminalPalette\AnsiSeq;
59+
use PHPLRPM\IPCUtilities;
5960

60-
$socket = findLrpmUnixSocket();
61+
$socket = UnixSocketStreamClient::findSocketPath('control', IPCUtilities::getSocketDirs());
62+
if (is_null($socket)) {
63+
exitNoConnection();
64+
}
6165
$client = new UnixSocketStreamClient($socket);
6266
if ($client->connect() === false) {
6367
exitNoConnection();
@@ -91,32 +95,6 @@ function probeForAutoloader($programName): void
9195
exit(EXIT_AUTOLOADER_NOT_FOUND);
9296
}
9397

94-
function findLrpmUnixSocket()
95-
{
96-
$socketDirs = [
97-
'/run/php-lrpm',
98-
'/run/user/' . posix_geteuid() . '/php-lrpm'
99-
];
100-
$socketFileName = 'control';
101-
foreach ($socketDirs as $dir) {
102-
$candidate = $dir . '/' . $socketFileName;
103-
if (is_writable($candidate)) {
104-
return $candidate;
105-
}
106-
}
107-
fwrite(STDERR, "Could not find lrpm Unix domain socket" . PHP_EOL);
108-
fwrite(
109-
STDERR,
110-
"Tried: " . implode(
111-
', ',
112-
array_map(function ($dir) use ($socketFileName) {
113-
return $dir . '/' . $socketFileName;
114-
}, $socketDirs)
115-
) . PHP_EOL
116-
);
117-
exitNoConnection();
118-
}
119-
12098
function exitNoConnection(): void
12199
{
122100
fwrite(STDERR, 'Could not connect to lrpm' . PHP_EOL);

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@
3333
"ext-json": "*",
3434
"ext-pcntl": "*",
3535
"ext-posix": "*",
36+
"ext-sockets": "*",
3637
"vrza/array-with-secondary-keys": "dev-main",
3738
"vrza/cardinal-collections": "dev-main",
3839
"vrza/php-tipc": "dev-main",
39-
"vrza/text-table-formatter": "dev-main",
40-
"vrza/terminal-palette": "dev-main"
40+
"vrza/terminal-palette": "dev-main",
41+
"vrza/text-table-formatter": "dev-main"
4142
},
4243
"autoload": {
4344
"psr-4": {

example.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@
55
use PHPLRPM\Test\MockConfigurationSource;
66
use PHPLRPM\ProcessManager;
77

8-
$configurationSource = new MockConfigurationSource();
9-
$processManager = new ProcessManager($configurationSource);
8+
$processManager = new ProcessManager(MockConfigurationSource::class);
109
$processManager->run();

src/ConfigurationClient.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace PHPLRPM;
4+
5+
use TIPC\UnixSocketStreamClient;
6+
7+
class ConfigurationClient
8+
{
9+
private $configSocket;
10+
11+
public function findConfigSocket()
12+
{
13+
$this->configSocket = UnixSocketStreamClient::findSocketPath(
14+
ConfigurationService::SOCKET_FILE_NAME,
15+
IPCUtilities::getSocketDirs()
16+
);
17+
if (is_null($this->configSocket)) {
18+
throw new RuntimeException("Supervisor could not find config process Unix domain socket");
19+
}
20+
}
21+
22+
/**
23+
* Creates a configuration client and contacts the configuration
24+
* process with a request to poll for latest configuration.
25+
*
26+
* @return array containing latest configuration
27+
* @throws ConfigurationPollException
28+
*/
29+
public function pollConfiguration($signalHandlers): array
30+
{
31+
$recvBufSize = 8 * 1024 * 1024;
32+
$client = new UnixSocketStreamClient($this->configSocket, $recvBufSize);
33+
if ($client->connect() === false) {
34+
throw new ConfigurationPollException("Could not connect to socket {$this->configSocket}");
35+
}
36+
if ($client->sendMessage(ConfigurationService::REQ_POLL_CONFIG_SOURCE) === false) {
37+
$client->disconnect();
38+
throw new ConfigurationPollException("Could not send config query message over socket {$this->configSocket}");
39+
}
40+
$signals = array_keys($signalHandlers);
41+
pcntl_sigprocmask(SIG_BLOCK, $signals);
42+
if (empty($response = $client->receiveMessage())) {
43+
pcntl_sigprocmask(SIG_UNBLOCK, $signals);
44+
$client->disconnect();
45+
throw new ConfigurationPollException("Failed to read config response from {$this->configSocket}");
46+
}
47+
pcntl_sigprocmask(SIG_UNBLOCK, $signals);
48+
$client->disconnect();
49+
if ($response === ConfigurationService::RESP_ERROR_CONFIG_SOURCE) {
50+
throw new ConfigurationPollException("Config process at {$this->configSocket} responded with an error message");
51+
}
52+
return Serialization::deserialize($response);
53+
}
54+
55+
}

src/ConfigurationPollException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace PHPLRPM;
4+
5+
use Exception;
6+
7+
class ConfigurationPollException extends Exception
8+
{
9+
10+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace PHPLRPM;
4+
5+
class ConfigurationProcessManager
6+
{
7+
private const CONFIG_PROCESS_MAX_BACKOFF_SECONDS = 300;
8+
private const CONFIG_PROCESS_MAX_RETRIES = 5;
9+
private const CONFIG_PROCESS_MIN_RUN_TIME_SECONDS = 40;
10+
private const CONFIG_PROCESS_TERM_TIMEOUT_SECONDS = 5;
11+
12+
private $configProcessId;
13+
private $configProcessRetries = 0;
14+
private $configProcessLastStart = 0;
15+
private $configProcessRestartAt = 0;
16+
17+
public function getPID(): ?int
18+
{
19+
return $this->configProcessId;
20+
}
21+
22+
public function setPID($pid): void
23+
{
24+
$this->configProcessId = $pid;
25+
}
26+
27+
public function handleTerminatedConfigProcess(): void
28+
{
29+
$this->configProcessId = null;
30+
$this->scheduleRestartWithBackoff();
31+
}
32+
33+
private function scheduleRestartWithBackoff()
34+
{
35+
$now = time();
36+
if ($now - $this->configProcessLastStart >= self::CONFIG_PROCESS_MIN_RUN_TIME_SECONDS) {
37+
$this->configProcessRetries = 0;
38+
} else {
39+
$backoff = min(2 ** $this->configProcessRetries, self::CONFIG_PROCESS_MAX_BACKOFF_SECONDS);
40+
fwrite(STDERR, "==> Backing off on config process spawn (retry: " . $this->configProcessRetries . ", seconds: $backoff)" . PHP_EOL);
41+
$this->configProcessRestartAt = $now + $backoff;
42+
$this->configProcessRetries++;
43+
}
44+
}
45+
46+
public function shouldRetryStartingConfigProcess(): ?bool
47+
{
48+
if (!is_null($this->configProcessId)) {
49+
return false;
50+
}
51+
if ($this->configProcessRetries > self::CONFIG_PROCESS_MAX_RETRIES) {
52+
fwrite(STDERR, '==> Config process failed after ' . self::CONFIG_PROCESS_MAX_RETRIES . ' retries, giving up' . PHP_EOL);
53+
return null;
54+
}
55+
$now = time();
56+
if ($this->configProcessRestartAt < $now) {
57+
$this->configProcessLastStart = $now;
58+
return true;
59+
}
60+
return false;
61+
}
62+
63+
public function stopConfigurationProcess(): void
64+
{
65+
if (is_null($this->configProcessId)) {
66+
return;
67+
}
68+
posix_kill($this->configProcessId, SIGTERM);
69+
$remaining = self::CONFIG_PROCESS_TERM_TIMEOUT_SECONDS;
70+
while (!is_null($this->configProcessId) && $remaining > 0) {
71+
$remaining = sleep($remaining);
72+
pcntl_signal_dispatch();
73+
}
74+
if (!is_null($this->configProcessId)) {
75+
posix_kill($this->configProcessId, SIGKILL);
76+
}
77+
}
78+
79+
}

0 commit comments

Comments
 (0)