Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions config/install/ultimate_cron.job.portfolio_refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
langcode: en
status: true
dependencies:
module:
- oit
title: 'Portfolio Refresh'
Comment thread
protitude marked this conversation as resolved.
id: portfolio_refresh
weight: 12
module: oit
callback: '\Drupal\oit\Services\CronManager::refreshPortfolio'
scheduler:
id: crontab
configuration:
rules:
- '0 */8 * * *'
catch_up: 28800
launcher:
id: serial
configuration:
timeouts:
lock_timeout: 3600
launcher:
thread: 0
logger:
id: database
configuration:
method: '3'
expire: 1209600
retain: 1000
1 change: 1 addition & 0 deletions oit.info.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ type: module
description: 'Replacement for OIT General module in D7. General functionality will be placed in here.'
dependencies:
- drupal:block
- ultimate_cron:ultimate_cron

core_version_requirement: ^11
3 changes: 3 additions & 0 deletions oit.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ services:
oit.domain:
class: Drupal\oit\Plugin\Domain
arguments: ["@token"]
oit.portfolio:
class: Drupal\oit\Services\PortfolioData
arguments: ['@cache.default', '@cache_tags.invalidator']
oit.route_subscriber:
class: Drupal\oit\Services\RouteSubscriber
tags:
Expand Down
56 changes: 52 additions & 4 deletions src/Plugin/Block/PortfolioBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

use Drupal\Component\Utility\Xss;
use Drupal\Core\Block\BlockBase;
use Drupal\oit\Plugin\GoogleSheetsFetch;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\oit\Plugin\GoogleSheetsProcess;
use Drupal\oit\Services\PortfolioData;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* OIT Projects Portfolio block.
Expand All @@ -16,7 +18,43 @@
* Report")
* )
*/
class PortfolioBlock extends BlockBase {
class PortfolioBlock extends BlockBase implements ContainerFactoryPluginInterface {

/**
* The portfolio data service.
*
* @var \Drupal\oit\Services\PortfolioData
*/
protected $portfolioData;

/**
* Constructs a new PortfolioBlock.
*
* @param array $configuration
* The plugin configuration.
* @param string $plugin_id
* The plugin ID.
* @param mixed $plugin_definition
* The plugin definition.
* @param \Drupal\oit\Services\PortfolioData $portfolio_data
* The portfolio data service.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, PortfolioData $portfolio_data) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->portfolioData = $portfolio_data;
}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('oit.portfolio')
);
}

/**
* {@inheritdoc}
Expand All @@ -34,8 +72,10 @@ public function build() {
* Render array for the portfolio table.
*/
public function fetchPortfolio() {
$fetchData = new GoogleSheetsFetch('1k4-Csp29uLZbh_g2nhuhpq3dVBgZ6zWFK20BXP1rL_s', 0, 0);
$gsheet_returned_data = $fetchData->getFetchedSheet();
// The raw sheet data is fetched and cached by the oit.portfolio service so
// the slow remote request happens off the request-render path (warmed by
// cron) rather than on every render-cache miss.
$gsheet_returned_data = $this->portfolioData->getSheetData();
$processData = new GoogleSheetsProcess($gsheet_returned_data, 'a,b,c,d,e,f,g,h,i,j', 'custom');
$data = $processData->getProcessedData();
$header = [
Expand Down Expand Up @@ -122,6 +162,14 @@ public function fetchPortfolio() {
'library' => ['oit/table_search', 'oit/oit_projects'],
],
];
// Set a single top-level cache entry so the tag bubbles to the node page
// render cache, the anonymous Internal Page Cache and the Pantheon Global
// CDN. The max-age is a freshness backstop only — the remote fetch is gated
// behind the oit.portfolio data cache, not this value.
$html['#cache'] = [
'tags' => [PortfolioData::TAG],
'max-age' => PortfolioData::TTL,
];
return $html;
}

Expand Down
15 changes: 15 additions & 0 deletions src/Services/CronManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,21 @@ public static function topTutorials() {
}
}

/**
* Warm the Portfolio block cache by re-fetching the Google Sheet.
*
* Live environment only. In non-live environments the block's cold-cache
* fallback fetch handles dev/test.
*/
public static function refreshPortfolio() {
$env = getenv('PANTHEON_ENVIRONMENT');
if ($env !== 'live') {
\Drupal::logger('oit')->notice('Portfolio refresh skipped. This is the @env environment.', ['@env' => $env]);
return;
}
\Drupal::service('oit.portfolio')->refresh();
}

/**
* Delete old news nodes that are not linked anywhere on the site.
*
Expand Down
141 changes: 141 additions & 0 deletions src/Services/PortfolioData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

namespace Drupal\oit\Services;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\oit\Plugin\GoogleSheetsFetch;

/**
* Shared fetch/cache layer for the OIT Project Portfolio sheet data.
*
* Used by both the PortfolioBlock (read path) and the portfolio_refresh cron
* job (write path) so the remote Google Sheets fetch is performed off the
* request-render path and the cache is warmed on a schedule.
*/
class PortfolioData {

/**
* The Google Sheets document key.
*/
const SHEET_KEY = '1k4-Csp29uLZbh_g2nhuhpq3dVBgZ6zWFK20BXP1rL_s';

/**
* The Google Sheets tab GID.
*/
const SHEET_GID = 0;

/**
* The cache ID for the raw fetched sheet data.
*/
const CID = 'oit:portfolio_block:sheet_data';

/**
* The cache tag invalidated when fresh data is written.
*/
const TAG = 'oit:portfolio';

/**
* The cache TTL in seconds (26 hours).
*/
const TTL = 93600;

/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cacheBackend;

/**
* The cache tags invalidator.
*
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
*/
protected $cacheTagsInvalidator;

/**
* Constructs a new PortfolioData service.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* The cache backend.
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
* The cache tags invalidator.
*/
public function __construct(CacheBackendInterface $cache_backend, CacheTagsInvalidatorInterface $cache_tags_invalidator) {
$this->cacheBackend = $cache_backend;
$this->cacheTagsInvalidator = $cache_tags_invalidator;
}

/**
* Gets the raw sheet data, populating the cache on a cold miss.
*
* On a cold miss this performs a one-off live fetch and caches a non-empty
* result for 26 hours so the table is never empty on a successful fetch. It
* does not invalidate the cache tag — the render that triggered it caches
* itself fresh anyway.
*
* @return array
* The raw fetched sheet data.
*/
public function getSheetData(): array {
if ($cached = $this->cacheBackend->get(self::CID)) {
return $cached->data;
}

$data = $this->fetchRaw();
$this->writeCache($data);
return $data;
}

/**
* Forces a live fetch and warms the cache.
*
* Writes a non-empty result for 26 hours and, when the write actually
* happened, invalidates the cache tag so any rendered block rebuilds
* immediately with fresh data. On an empty fetch nothing is written and the
* tag is not invalidated, leaving the warm data and render cache untouched.
*/
public function refresh(): void {
$data = $this->fetchRaw();
if ($this->writeCache($data)) {
$this->cacheTagsInvalidator->invalidateTags([self::TAG]);
}
}

/**
* Performs the live network fetch of the sheet.
*
* Isolated as the single network seam so unit tests can override it with
* canned or empty data.
*
* @return array
* The raw fetched sheet data.
*/
protected function fetchRaw(): array {
return (new GoogleSheetsFetch(self::SHEET_KEY, self::SHEET_GID, 0))->getFetchedSheet();
}

/**
* Writes the data to the cache, guarding against empty results.
*
* Never caches an empty result so a transient upstream failure cannot pin an
* empty table or clobber a good 26h entry. Centralizes the non-empty guard
* and the 26h TTL so they cannot drift apart between the read and write
* paths.
*
* @param array $data
* The data to cache.
*
* @return bool
* TRUE if the cache was written, FALSE if the data was empty.
*/
private function writeCache(array $data): bool {
if (empty($data)) {
return FALSE;
}
$this->cacheBackend->set(self::CID, $data, time() + self::TTL);
return TRUE;
}

}
Loading
Loading