feat(store): fetch contributions incrementally and persist history in SQLite
Bound each provider's startFetch()/resolveFetch() to an optional since/until window, wire a SQLite-backed ContributionStore into ContributionAggregator (fetch only the trailing window past the last stored date, merge into the store, prune by CONTRIBUTIONS_RETENTION_DAYS), and add a graph:contributions:refetch command to force a full or ranged re-fetch in <=365-day chunks. This is the root fix for the full 365-day-refetch timeout that used to hit on every cache miss.
This commit is contained in:
@@ -10,25 +10,64 @@ use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||
|
||||
final class ContributionAggregator
|
||||
{
|
||||
private const RENDER_WINDOW_DAYS = 371;
|
||||
private const OVERLAP_DAYS = 3;
|
||||
|
||||
/** @param iterable<ProviderInterface> $providers */
|
||||
public function __construct(
|
||||
#[AutowireIterator('app.provider')]
|
||||
private readonly iterable $providers,
|
||||
private readonly ContributionStore $store,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {}
|
||||
|
||||
/** @return array<string, int> */
|
||||
/**
|
||||
* Incrementally fetches fresh contributions from each configured provider, merges them into
|
||||
* the store, and returns the rolling render window summed across providers.
|
||||
*
|
||||
* For each provider, only data since its last stored date (minus a trailing overlap) is
|
||||
* fetched; a provider with no stored history yet fetches its full default range. Fetches are
|
||||
* split into start/resolve phases so all providers' requests are in flight concurrently.
|
||||
* Failures are logged and the affected provider is skipped, not fatal to the others.
|
||||
*
|
||||
* @return array<string, int> date (Y-m-d) => contribution count, summed across all providers
|
||||
*/
|
||||
public function aggregate(): array
|
||||
{
|
||||
$pending = [];
|
||||
$configured = [];
|
||||
|
||||
/** @var ProviderInterface $provider */
|
||||
foreach ($this->providers as $provider) {
|
||||
if (!$provider->isConfigured()) {
|
||||
continue;
|
||||
if ($provider->isConfigured()) {
|
||||
$configured[] = $provider;
|
||||
}
|
||||
}
|
||||
|
||||
$pending = [];
|
||||
|
||||
foreach ($configured as $provider) {
|
||||
$latest = $this->store->latestDate($provider->getName());
|
||||
$since = $latest !== null
|
||||
? (new \DateTimeImmutable('@' . $latest))->modify('-' . self::OVERLAP_DAYS . ' days')
|
||||
: null;
|
||||
|
||||
try {
|
||||
$pending[] = [$provider, $provider->startFetch()];
|
||||
$pending[] = [$provider, $provider->startFetch($since)];
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning(sprintf('%s fetch failed: %s', $provider::class, $e->getMessage()), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($pending as [$provider, $handle]) {
|
||||
try {
|
||||
$fresh = $provider->resolveFetch($handle);
|
||||
|
||||
$dateCounts = [];
|
||||
foreach ($fresh as $date => $count) {
|
||||
$dateCounts[(new \DateTimeImmutable($date))->getTimestamp()] = $count;
|
||||
}
|
||||
|
||||
$this->store->merge($provider->getName(), $dateCounts);
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning(sprintf('%s fetch failed: %s', $provider::class, $e->getMessage()), ['exception' => $e]);
|
||||
}
|
||||
@@ -36,16 +75,15 @@ final class ContributionAggregator
|
||||
|
||||
$contributions = [];
|
||||
|
||||
foreach ($pending as [$provider, $handle]) {
|
||||
try {
|
||||
foreach ($provider->resolveFetch($handle) as $date => $count) {
|
||||
$contributions[$date] = ($contributions[$date] ?? 0) + $count;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->warning(sprintf('%s fetch failed: %s', $provider::class, $e->getMessage()), ['exception' => $e]);
|
||||
foreach ($configured as $provider) {
|
||||
foreach ($this->store->all($provider->getName(), sinceDays: self::RENDER_WINDOW_DAYS) as $contribution) {
|
||||
$date = (new \DateTimeImmutable('@' . $contribution->date))->format('Y-m-d');
|
||||
$contributions[$date] = ($contributions[$date] ?? 0) + $contribution->count;
|
||||
}
|
||||
}
|
||||
|
||||
$this->store->prune();
|
||||
|
||||
return $contributions;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user