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: // @phpstan-ignore return.unusedType
78: protected function getLongDescription(): ?string
79: {
80: if ($this->Providers) {
81: $description[] = <<<EOF
82: If no providers are given, all providers are checked.
83: EOF;
84: }
85:
86: $description[] = <<<EOF
87: If a heartbeat request fails, __{{subcommand}}__ continues to the next provider
88: unless `-f/--fail-early` is given, in which case it exits immediately.
89:
90: The command exits with a non-zero status if a provider backend is unreachable.
91: EOF;
92:
93: return implode(\PHP_EOL . \PHP_EOL, $description);
94: }
95:
96: protected function run(string ...$args)
97: {
98: $this->startRun();
99:
100: if ($this->Providers) {
101: $providers = array_values(array_map(
102: fn(string $providerClass) =>
103: $this->App->get($providerClass),
104: array_intersect_key(
105: $this->Providers,
106: array_flip($this->ProviderBasename),
107: ),
108: ));
109: } else {
110: $providers = array_values(array_map(
111: function (string $providerClass) {
112: if (is_a(
113: $this->App->getName($providerClass),
114: SyncProviderInterface::class,
115: true
116: )) {
117: if (!$this->App->has($providerClass)) {
118: $this->App->singleton($providerClass);
119: }
120: return $this->App->get($providerClass);
121: }
122:
123: throw new CliInvalidArgumentsException(sprintf(
124: '%s does not implement %s',
125: $providerClass,
126: SyncProviderInterface::class,
127: ));
128: },
129: $this->Provider,
130: ));
131: }
132:
133: $count = count($providers);
134:
135: Console::info(Inflect::format(
136: $count,
137: 'Sending heartbeat request to {{#}} {{#:provider}}',
138: ));
139:
140: $this->Store->checkProviderHeartbeats(
141: max(1, $this->Ttl),
142: $this->FailEarly,
143: ...$providers,
144: );
145:
146: Console::summary(Inflect::format(
147: $count,
148: '{{#}} {{#:provider}} checked',
149: ));
150: }
151: }
152: