gdbots/ncr-php

View on GitHub
src/NcrRequestInterceptor.php

Summary

Maintainability
A
25 mins
Test Coverage
<?php
declare(strict_types=1);

namespace Gdbots\Ncr;

use Gdbots\Pbj\Message;
use Gdbots\Pbj\WellKnown\NodeRef;
use Gdbots\Pbjx\Event\GetResponseEvent;
use Gdbots\Pbjx\Event\PbjxEvent;
use Gdbots\Pbjx\Event\ResponseCreatedEvent;
use Gdbots\Pbjx\EventSubscriber;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;

class NcrRequestInterceptor implements EventSubscriber
{
    protected CacheItemPoolInterface $cache;
    protected NcrCache $ncrCache;

    /**
     * If a request for nodes contains items already available in NcrCache
     * then it will store the NodeRefs here and pick them up from cache
     * on the response created handler.
     *
     * @var array
     */
    protected array $pickup = [];

    /**
     * When get node requests occur with slugs we will attempt
     * to cache the slug -> node_ref, these are there stories...
     * boom boom.
     *
     * @var CacheItemInterface[]
     */
    protected array $cacheItems = [];

    public static function getSubscribedEvents(): array
    {
        return [
            'gdbots:ncr:mixin:get-node-request.enrich'              => 'enrichGetNodeRequest',
            'gdbots:ncr:mixin:get-node-batch-request.before_handle' => 'onGetNodeBatchRequestBeforeHandle',
            'gdbots:ncr:mixin:get-node-response.created'            => 'onGetNodeResponseCreated',
            'gdbots:ncr:mixin:get-node-batch-response.created'      => 'onGetNodeBatchResponseCreated',
        ];
    }

    public function __construct(CacheItemPoolInterface $cache, NcrCache $ncrCache)
    {
        $this->cache = $cache;
        $this->ncrCache = $ncrCache;
    }

    /**
     * Enrich the request with a node_ref if possible to minimize
     * the number of lookups against the slug secondary index.
     *
     * @param PbjxEvent $pbjxEvent
     */
    public function enrichGetNodeRequest(PbjxEvent $pbjxEvent): void
    {
        $request = $pbjxEvent->getMessage();
        if ($request->has('node_ref')
            || $request->get('consistent_read')
            || !$request->has('qname')
            || !$request->has('slug')
        ) {
            return;
        }

        $cacheKey = $this->getSlugCacheKey($request);
        $cacheItem = $this->cache->getItem($cacheKey);
        if (!$cacheItem->isHit()) {
            $this->cacheItems[$cacheKey] = $cacheItem;
            return;
        }

        try {
            $nodeRef = NodeRef::fromString($cacheItem->get());
            if ($nodeRef->getQName()->toString() === $request->get('qname')) {
                $request->set('node_ref', $nodeRef);
            }
        } catch (\Throwable $e) {
        }
    }

    public function onGetNodeBatchRequestBeforeHandle(GetResponseEvent $pbjxEvent): void
    {
        $request = $pbjxEvent->getRequest();
        if ($request->get('consistent_read') || !$request->has('node_refs')) {
            return;
        }

        $ncrCachedNodeRefs = [];

        /** @var NodeRef $nodeRef */
        foreach ($request->get('node_refs') as $nodeRef) {
            if ($this->ncrCache->hasNode($nodeRef)) {
                $ncrCachedNodeRefs[] = $nodeRef;
            }
        }

        if (empty($ncrCachedNodeRefs)) {
            return;
        }

        $request->removeFromSet('node_refs', $ncrCachedNodeRefs);
        $this->pickup[(string)$request->get('request_id')] = $ncrCachedNodeRefs;
    }

    public function onGetNodeResponseCreated(ResponseCreatedEvent $pbjxEvent): void
    {
        $response = $pbjxEvent->getResponse();
        if (!$response->has('node')) {
            return;
        }

        $node = $response->get('node');
        $this->ncrCache->addNode($node);

        $request = $pbjxEvent->getRequest();
        if (!$request->has('qname') || !$request->has('slug')) {
            // was not a slug lookup
            return;
        }

        if ($node->get('slug') !== $request->get('slug')) {
            // for some reason, the node we got ain't the one we want
            return;
        }

        $nodeRef = NodeRef::fromNode($node);
        $cacheKey = $this->getSlugCacheKey($request);
        if (!isset($this->cacheItems[$cacheKey])) {
            // lookup never happend
            return;
        }

        $cacheItem = $this->cacheItems[$cacheKey];
        $cacheItem->set($nodeRef->toString())->expiresAfter($this->getSlugCacheTtl($request));
        unset($this->cacheItems[$cacheKey]);
        $this->cache->saveDeferred($cacheItem);
    }

    public function onGetNodeBatchResponseCreated(ResponseCreatedEvent $pbjxEvent): void
    {
        $response = $pbjxEvent->getResponse();
        if ($response->has('nodes')) {
            $this->ncrCache->addNodes($response->get('nodes'));
        }

        $requestId = $response->get('ctx_request_ref')->getId();
        if (!isset($this->pickup[$requestId])) {
            return;
        }

        /** @var NodeRef $nodeRef */
        foreach ($this->pickup[$requestId] as $nodeRef) {
            try {
                $response->addToMap('nodes', $nodeRef->toString(), $this->ncrCache->getNode($nodeRef));
            } catch (\Throwable $e) {
                $response->addToSet('missing_node_refs', [$nodeRef]);
            }
        }

        unset($this->pickup[$requestId]);
    }

    protected function getSlugCacheKey(Message $request): string
    {
        return str_replace('-', '_', sprintf(
            'stnr.%s.%s',
            str_replace(':', '.', $request->get('qname')),
            md5($request->get('slug'))
        ));
    }

    protected function getSlugCacheTtl(Message $request): int
    {
        return 300;
    }
}