1: <?php declare(strict_types=1);
2:
3: namespace Salient\Sync\Command;
4:
5: use Salient\Cli\Exception\CliInvalidArgumentsException;
6: use Salient\Cli\CliOption;
7: use Salient\Contract\Cli\CliOptionType;
8: use Salient\Contract\Cli\CliOptionValueType;
9: use Salient\Contract\Sync\SyncProviderInterface;
10: use Salient\Core\Facade\Console;
11: use Salient\Utility\Inflect;
12:
13: /**
14: * A generic sync provider heartbeat check command
15: *
16: * @api
17: */
18: final class CheckSyncProviderHeartbeat extends AbstractSyncCommand
19: {
20: /** @var string[] */
21: private array $ProviderBasename = [];
22: /** @var array<class-string<SyncProviderInterface>> */
23: private array $Provider = [];
24: private int $Ttl = 0;
25: private bool $FailEarly = false;
26:
27: public function getDescription(): string
28: {
29: return 'Send a heartbeat request to ' . (
30: $this->Providers
31: ? 'registered providers'
32: : 'one or more providers'
33: );
34: }
35:
36: protected function getOptionList(): iterable
37: {
38: $builder = CliOption::build()
39: ->name('provider')
40: ->multipleAllowed();
41:
42: if ($this->Providers) {
43: yield $builder
44: ->optionType(CliOptionType::ONE_OF_POSITIONAL)
45: ->allowedValues(array_keys($this->Providers))
46: ->addAll()
47: ->defaultValue('ALL')
48: ->bindTo($this->ProviderBasename);
49: } else {
50: yield $builder
51: ->description('The fully-qualified name of the provider to check')
52: ->optionType(CliOptionType::VALUE_POSITIONAL)
53: ->required()
54: ->bindTo($this->Provider);
55: }
56:
57: yield from [
58: CliOption::build()
59: ->long('ttl')
60: ->short('t')
61: ->valueName('seconds')
62: ->description('The lifetime of a positive result, in seconds')
63: ->optionType(CliOptionType::VALUE)
64: ->valueType(CliOptionValueType::INTEGER)
65: ->defaultValue(300)
66: ->bindTo($this->Ttl),
67: CliOption::build()
68: ->long('fail-early')
69: ->short('f')
70: ->description('If a check fails, exit without checking other providers')
71: ->bindTo($this->FailEarly),
72: ];
73:
74: yield from $this->getGlobalOptionList();
75: }
76:
77: public function getLongDescription(): ?string
78: {
79: if ($this->Providers) {
80: $description[] = <<<EOF
81: If no providers are given, all providers are checked.
82: EOF;
83: }
84:
85: $description[] = <<<EOF
86: If a heartbeat request fails, __{{subcommand}}__ continues to the next provider
87: unless `-f/--fail-early` is given, in which case it exits immediately.
88:
89: The command exits with a non-zero status if a provider backend is unreachable.
90: EOF;
91:
92: return implode(\PHP_EOL . \PHP_EOL, $description);
93: }
94:
95: protected function run(string ...$args)
96: {
97: $this->startRun();
98:
99: if ($this->Providers) {
100: $providers = array_values(array_map(
101: fn(string $providerClass) =>
102: $this->App->get($providerClass),
103: array_intersect_key(
104: $this->Providers,
105: array_flip($this->ProviderBasename),
106: ),
107: ));
108: } else {
109: $providers = array_values(array_map(
110: function (string $providerClass) {
111: if (is_a(
112: $this->App->getName($providerClass),
113: SyncProviderInterface::class,
114: true
115: )) {
116: if (!$this->App->has($providerClass)) {
117: $this->App->singleton($providerClass);
118: }
119: return $this->App->get($providerClass);
120: }
121:
122: throw new CliInvalidArgumentsException(sprintf(
123: '%s does not implement %s',
124: $providerClass,
125: SyncProviderInterface::class,
126: ));
127: },
128: $this->Provider,
129: ));
130: }
131:
132: $count = count($providers);
133:
134: Console::info(Inflect::format(
135: $count,
136: 'Sending heartbeat request to {{#}} {{#:provider}}',
137: ));
138:
139: $this->Store->checkProviderHeartbeats(
140: max(1, $this->Ttl),
141: $this->FailEarly,
142: ...$providers,
143: );
144:
145: Console::summary(Inflect::format(
146: $count,
147: '{{#}} {{#:provider}} checked',
148: ));
149: }
150: }
151: