Browse Source

Optimize

imwald
Nuša Pukšič 1 month ago
parent
commit
acc2d15216
  1. 37
      src/Command/CacheLatestArticlesCommand.php
  2. 27
      src/Command/NostrRelayPoolStatsCommand.php
  3. 48
      src/Controller/DefaultController.php
  4. 24
      src/Service/NostrClient.php
  5. 95
      src/Service/NostrRelayPool.php

37
src/Command/CacheLatestArticlesCommand.php

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Command; namespace App\Command;
use App\Service\RedisCacheService;
use App\Util\NostrKeyUtil;
use FOS\ElasticaBundle\Finder\FinderInterface; use FOS\ElasticaBundle\Finder\FinderInterface;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -23,13 +25,20 @@ class CacheLatestArticlesCommand extends Command
private FinderInterface $finder; private FinderInterface $finder;
private CacheItemPoolInterface $articlesCache; private CacheItemPoolInterface $articlesCache;
private ParameterBagInterface $params; private ParameterBagInterface $params;
private RedisCacheService $redisCacheService;
public function __construct(FinderInterface $finder, CacheItemPoolInterface $articlesCache, ParameterBagInterface $params) public function __construct(
FinderInterface $finder,
CacheItemPoolInterface $articlesCache,
ParameterBagInterface $params,
RedisCacheService $redisCacheService
)
{ {
parent::__construct(); parent::__construct();
$this->finder = $finder; $this->finder = $finder;
$this->articlesCache = $articlesCache; $this->articlesCache = $articlesCache;
$this->params = $params; $this->params = $params;
$this->redisCacheService = $redisCacheService;
} }
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
@ -70,10 +79,34 @@ class CacheLatestArticlesCommand extends Command
$articles = $this->finder->find($query); $articles = $this->finder->find($query);
// Pre-fetch and cache author metadata for all articles
$authorPubkeys = [];
foreach ($articles as $article) {
// Debug: check what type of object we have
if ($article instanceof \App\Entity\Article) {
$pubkey = $article->getPubkey();
if ($pubkey && NostrKeyUtil::isHexPubkey($pubkey)) {
$authorPubkeys[] = $pubkey;
}
} elseif (is_object($article)) {
// Elastica result object
if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) {
$authorPubkeys[] = $article->pubkey;
} elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) {
$authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub);
}
}
}
$authorPubkeys = array_unique($authorPubkeys);
$output->writeln('<comment>Pre-fetching metadata for ' . count($authorPubkeys) . ' authors...</comment>');
$authorsMetadata = $this->redisCacheService->getMultipleMetadata($authorPubkeys);
$output->writeln('<comment>Fetched ' . count($authorsMetadata) . ' author profiles.</comment>');
$cacheItem->set($articles); $cacheItem->set($articles);
$cacheItem->expiresAfter(3600); // Cache for 1 hour $cacheItem->expiresAfter(3600); // Cache for 1 hour
$this->articlesCache->save($cacheItem); $this->articlesCache->save($cacheItem);
$output->writeln('<info>Cached ' . count($articles) . ' articles.</info>'); $output->writeln('<info>Cached ' . count($articles) . ' articles with author metadata.</info>');
// } else { // } else {
// $output->writeln('<comment>Cache already exists for key: ' . $cacheKey . '</comment>'); // $output->writeln('<comment>Cache already exists for key: ' . $cacheKey . '</comment>');
// } // }

27
src/Command/NostrRelayPoolStatsCommand.php

@ -26,10 +26,35 @@ class NostrRelayPoolStatsCommand extends Command
$io = new SymfonyStyle($input, $output); $io = new SymfonyStyle($input, $output);
$stats = $this->relayPool->getStats(); $stats = $this->relayPool->getStats();
$localRelay = $this->relayPool->getLocalRelay();
$defaultRelays = $this->relayPool->getDefaultRelays();
$io->title('Nostr Relay Pool Statistics'); $io->title('Nostr Relay Pool Statistics');
$io->section('Overview'); $io->section('Configuration');
$io->table(
['Setting', 'Value'],
[
['Local Relay', $localRelay ?: '(not configured)'],
['Default Relays', count($defaultRelays)],
]
);
if (!empty($defaultRelays)) {
$io->section('Default Relay List (Priority Order)');
$rows = [];
foreach ($defaultRelays as $index => $relay) {
$isLocal = $localRelay && $relay === $localRelay;
$rows[] = [
$index + 1,
$relay,
$isLocal ? '✓ Local' : 'Public'
];
}
$io->table(['Priority', 'Relay URL', 'Type'], $rows);
}
$io->section('Connection Pool');
$io->table( $io->table(
['Metric', 'Value'], ['Metric', 'Value'],
[ [

48
src/Controller/DefaultController.php

@ -61,28 +61,28 @@ class DefaultController extends AbstractController
CacheItemPoolInterface $articlesCache CacheItemPoolInterface $articlesCache
): Response ): Response
{ {
set_time_limit(300);
ini_set('max_execution_time', '300');
$env = $this->getParameter('kernel.environment'); $env = $this->getParameter('kernel.environment');
// Reuse previous latest list cache key to show same set as old 'latest' // Reuse previous latest list cache key to show same set as old 'latest'
$cacheKey = 'latest_articles_list_' . $env; $cacheKey = 'latest_articles_list_' . $env;
$cacheItem = $articlesCache->getItem($cacheKey); $cacheItem = $articlesCache->getItem($cacheKey);
$key = new Key();
$excludedPubkeys = [
$key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'),
$key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'),
$key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'),
$key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'),
$key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'),
$key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'),
$key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'),
$key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'),
];
if (!$cacheItem->isHit()) { if (!$cacheItem->isHit()) {
// Fallback: run query now if command hasn't populated cache yet // Fallback: run query now if command hasn't populated cache yet
set_time_limit(300);
ini_set('max_execution_time', '300');
$key = new Key();
$excludedPubkeys = [
$key->convertToHex('npub1etsrcjz24fqewg4zmjze7t5q8c6rcwde5zdtdt4v3t3dz2navecscjjz94'),
$key->convertToHex('npub1m7szwpud3jh2k3cqe73v0fd769uzsj6rzmddh4dw67y92sw22r3sk5m3ys'),
$key->convertToHex('npub13wke9s6njrmugzpg6mqtvy2d49g4d6t390ng76dhxxgs9jn3f2jsmq82pk'),
$key->convertToHex('npub10akm29ejpdns52ca082skmc3hr75wmv3ajv4987c9lgyrfynrmdqduqwlx'),
$key->convertToHex('npub13uvnw9qehqkds68ds76c4nfcn3y99c2rl9z8tr0p34v7ntzsmmzspwhh99'),
$key->convertToHex('npub1fls5au5fxj6qj0t36sage857cs4tgfpla0ll8prshlhstagejtkqc9s2yl'),
$key->convertToHex('npub1t5d8kcn0hu8zmt6dpkgatd5hwhx76956g7qmdzwnca6fzgprzlhqnqks86'),
$key->convertToHex('npub14l5xklll5vxzrf6hfkv8m6n2gqevythn5pqc6ezluespah0e8ars4279ss'),
];
$boolQuery = new BoolQuery(); $boolQuery = new BoolQuery();
$boolQuery->addMustNot(new Query\Terms('pubkey', $excludedPubkeys)); $boolQuery->addMustNot(new Query\Terms('pubkey', $excludedPubkeys));
$query = new Query($boolQuery); $query = new Query($boolQuery);
@ -99,12 +99,22 @@ class DefaultController extends AbstractController
$articles = $cacheItem->get(); $articles = $cacheItem->get();
// Fetch author metadata - this is now much faster because
// metadata is pre-cached by the CacheLatestArticlesCommand
$authorPubkeys = []; $authorPubkeys = [];
foreach ($articles as $article) { foreach ($articles as $article) {
if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) { if ($article instanceof \App\Entity\Article) {
$authorPubkeys[] = $article->pubkey; $pubkey = $article->getPubkey();
} elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) { if ($pubkey && NostrKeyUtil::isHexPubkey($pubkey)) {
$authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub); $authorPubkeys[] = $pubkey;
}
} elseif (is_object($article)) {
// Elastica result object fallback
if (isset($article->pubkey) && NostrKeyUtil::isHexPubkey($article->pubkey)) {
$authorPubkeys[] = $article->pubkey;
} elseif (isset($article->npub) && NostrKeyUtil::isNpub($article->npub)) {
$authorPubkeys[] = NostrKeyUtil::npubToHex($article->npub);
}
} }
} }
$authorPubkeys = array_unique($authorPubkeys); $authorPubkeys = array_unique($authorPubkeys);

24
src/Service/NostrClient.php

@ -129,13 +129,13 @@ class NostrClient
*/ */
public function getPubkeyMetadata($pubkey): \stdClass public function getPubkeyMetadata($pubkey): \stdClass
{ {
// Use relay pool for all relays including purplepag.es // Use relay pool for all relays including purplepag.es, with local relay prioritized
$relayUrls = [ $relayUrls = $this->relayPool->ensureLocalRelayInList([
'wss://theforest.nostr1.com', 'wss://theforest.nostr1.com',
'wss://nostr.land', 'wss://nostr.land',
'wss://relay.primal.net', 'wss://relay.primal.net',
'wss://purplepag.es' 'wss://purplepag.es'
]; ]);
$relaySet = $this->createRelaySet($relayUrls); $relaySet = $this->createRelaySet($relayUrls);
$this->logger->info('Getting metadata for pubkey ' . $pubkey ); $this->logger->info('Getting metadata for pubkey ' . $pubkey );
@ -168,6 +168,9 @@ class NostrClient
if (empty($relays)) { if (empty($relays)) {
$key = new Key(); $key = new Key();
$relays = $this->getTopReputableRelaysForAuthor($key->convertPublicKeyToBech32($event->getPublicKey()), 5); $relays = $this->getTopReputableRelaysForAuthor($key->convertPublicKeyToBech32($event->getPublicKey()), 5);
} else {
// Ensure local relay is included when publishing
$relays = $this->relayPool->ensureLocalRelayInList($relays);
} }
// Use relay pool instead of creating new Relay instances // Use relay pool instead of creating new Relay instances
@ -206,8 +209,9 @@ class NostrClient
} }
$requestMessage = new RequestMessage($subscriptionId, [$filter]); $requestMessage = new RequestMessage($subscriptionId, [$filter]);
// Create relay set from all reputable relays on record // Create relay set from all reputable relays on record, with local relay prioritized
$relaySet = $this->createRelaySet(self::REPUTABLE_RELAYS); $relayUrls = $this->relayPool->ensureLocalRelayInList(self::REPUTABLE_RELAYS);
$relaySet = $this->createRelaySet($relayUrls);
$request = new Request($relaySet, $requestMessage); $request = new Request($relaySet, $requestMessage);
@ -281,6 +285,9 @@ class NostrClient
{ {
$this->logger->info('Getting event by ID', ['event_id' => $eventId, 'relays' => $relays]); $this->logger->info('Getting event by ID', ['event_id' => $eventId, 'relays' => $relays]);
// Ensure local relay is included in the relay list
$relays = $this->relayPool->ensureLocalRelayInList($relays);
// Merge relays with reputable ones // Merge relays with reputable ones
$allRelays = array_unique(array_merge($relays, self::REPUTABLE_RELAYS)); $allRelays = array_unique(array_merge($relays, self::REPUTABLE_RELAYS));
// Loop over reputable relays and bail as soon as you get a valid event back // Loop over reputable relays and bail as soon as you get a valid event back
@ -341,6 +348,11 @@ class NostrClient
$this->logger->info('Getting events by IDs', ['event_ids' => $eventIds, 'relays' => $relays]); $this->logger->info('Getting events by IDs', ['event_ids' => $eventIds, 'relays' => $relays]);
// Ensure local relay is included
if (!empty($relays)) {
$relays = $this->relayPool->ensureLocalRelayInList($relays);
}
// Use provided relays or default if empty // Use provided relays or default if empty
$relaySet = empty($relays) ? $this->defaultRelaySet : $this->createRelaySet($relays); $relaySet = empty($relays) ? $this->defaultRelaySet : $this->createRelaySet($relays);
@ -578,7 +590,7 @@ class NostrClient
$parts = explode(':', $coordinate, 3); $parts = explode(':', $coordinate, 3);
$pubkey = $parts[1]; $pubkey = $parts[1];
// Get author's relays for better chances of finding zaps // Get author's relays for better chances of finding zaps (includes local relay)
$authorRelays = $this->getTopReputableRelaysForAuthor($pubkey); $authorRelays = $this->getTopReputableRelaysForAuthor($pubkey);
$relaySet = $this->createRelaySet($authorRelays); $relaySet = $this->createRelaySet($authorRelays);

95
src/Service/NostrRelayPool.php

@ -23,7 +23,10 @@ class NostrRelayPool
private array $lastConnected = []; private array $lastConnected = [];
/** @var array<string> */ /** @var array<string> */
private array $defaultRelays = [ private array $defaultRelays = [];
/** @var array<string> Default public relays to fall back to */
private const PUBLIC_RELAYS = [
'wss://theforest.nostr1.com', 'wss://theforest.nostr1.com',
'wss://nostr.land', 'wss://nostr.land',
'wss://relay.primal.net', 'wss://relay.primal.net',
@ -39,11 +42,39 @@ class NostrRelayPool
private readonly string $nostrDefaultRelay, private readonly string $nostrDefaultRelay,
array $defaultRelays = [] array $defaultRelays = []
) { ) {
$relayList = $defaultRelays ?: $this->defaultRelays; // Build relay list: prioritize local relay, then custom relays, then public relays
if ($this->nostrDefaultRelay && !in_array($this->nostrDefaultRelay, $relayList, true)) { $relayList = [];
array_unshift($relayList, $this->nostrDefaultRelay);
// Add local relay first if configured
if ($this->nostrDefaultRelay) {
$relayList[] = $this->nostrDefaultRelay;
$this->logger->info('NostrRelayPool: Local relay configured as primary', [
'relay' => $this->nostrDefaultRelay
]);
}
// Add custom relays (excluding local relay if already added)
if (!empty($defaultRelays)) {
foreach ($defaultRelays as $relay) {
if (!in_array($relay, $relayList, true)) {
$relayList[] = $relay;
}
}
} else {
// Use public relays as fallback
foreach (self::PUBLIC_RELAYS as $relay) {
if (!in_array($relay, $relayList, true)) {
$relayList[] = $relay;
}
}
} }
$this->defaultRelays = $relayList; $this->defaultRelays = $relayList;
$this->logger->info('NostrRelayPool initialized with relay priority', [
'relay_count' => count($this->defaultRelays),
'relays' => $this->defaultRelays
]);
} }
/** /**
@ -88,7 +119,7 @@ class NostrRelayPool
} }
/** /**
* Get multiple relay connections, prioritizing default relay * Get multiple relay connections, prioritizing local relay (primary default relay)
* *
* @param array $relayUrls * @param array $relayUrls
* @return array<Relay> * @return array<Relay>
@ -96,26 +127,26 @@ class NostrRelayPool
public function getRelays(array $relayUrls): array public function getRelays(array $relayUrls): array
{ {
$relays = []; $relays = [];
$defaultRelay = $this->defaultRelays[0] ?? null; $localRelay = $this->nostrDefaultRelay ? $this->normalizeRelayUrl($this->nostrDefaultRelay) : null;
$relayUrlsNormalized = array_map([$this, 'normalizeRelayUrl'], $relayUrls); $relayUrlsNormalized = array_map([$this, 'normalizeRelayUrl'], $relayUrls);
$defaultRelayNormalized = $defaultRelay ? $this->normalizeRelayUrl($defaultRelay) : null;
// Try default relay first if present in requested URLs // Always try local relay first if configured, regardless of whether it's in the requested URLs
if ($defaultRelayNormalized && in_array($defaultRelayNormalized, $relayUrlsNormalized, true)) { if ($localRelay) {
try { try {
$relays[] = $this->getRelay($defaultRelayNormalized); $relays[] = $this->getRelay($localRelay);
$this->logger->debug('Using local relay (priority)', ['relay' => $localRelay]);
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->logger->warning('Default relay unavailable, falling back to others', [ $this->logger->warning('Local relay unavailable, falling back to others', [
'relay' => $defaultRelayNormalized, 'relay' => $localRelay,
'error' => $e->getMessage() 'error' => $e->getMessage()
]); ]);
} }
} }
// Add other relays except the default // Add other relays except the local relay
foreach ($relayUrlsNormalized as $url) { foreach ($relayUrlsNormalized as $url) {
if ($url === $defaultRelayNormalized) { if ($url === $localRelay) {
continue; continue; // Skip local relay as we already added it
} }
try { try {
$relays[] = $this->getRelay($url); $relays[] = $this->getRelay($url);
@ -126,6 +157,7 @@ class NostrRelayPool
]); ]);
} }
} }
return $relays; return $relays;
} }
@ -340,4 +372,37 @@ class NostrRelayPool
{ {
return $this->defaultRelays; return $this->defaultRelays;
} }
/**
* Get local relay URL if configured
*/
public function getLocalRelay(): ?string
{
return $this->nostrDefaultRelay ?: null;
}
/**
* Ensure local relay is included in a list of relay URLs
* Adds local relay at the beginning if configured and not already in the list
*/
public function ensureLocalRelayInList(array $relayUrls): array
{
if (!$this->nostrDefaultRelay) {
return $relayUrls;
}
$normalizedLocal = $this->normalizeRelayUrl($this->nostrDefaultRelay);
$normalizedUrls = array_map([$this, 'normalizeRelayUrl'], $relayUrls);
// If local relay not in list, add it at the beginning
if (!in_array($normalizedLocal, $normalizedUrls, true)) {
array_unshift($relayUrls, $this->nostrDefaultRelay);
$this->logger->debug('Added local relay to relay list', [
'local_relay' => $this->nostrDefaultRelay,
'total_relays' => count($relayUrls)
]);
}
return $relayUrls;
}
} }

Loading…
Cancel
Save