mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-12 10:45:54 +00:00
fix: add retry on kills retrieval (#207)
This commit is contained in:
@@ -2,6 +2,7 @@ import { SystemKillsContent } from '../../../mapInterface/widgets/SystemKills/Sy
|
|||||||
import { useKillsCounter } from '../../hooks/useKillsCounter';
|
import { useKillsCounter } from '../../hooks/useKillsCounter';
|
||||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
|
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
type TooltipSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
@@ -17,18 +18,45 @@ type KillsBookmarkTooltipProps = {
|
|||||||
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
|
export const KillsCounter = ({ killsCount, systemId, className, children, size = 'xs' }: KillsBookmarkTooltipProps) => {
|
||||||
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
|
const { isLoading, kills: detailedKills, systemNameMap } = useKillsCounter({ realSystemId: systemId });
|
||||||
|
|
||||||
if (!killsCount || detailedKills.length === 0 || !systemId || isLoading) return null;
|
// Limit the kills shown to match the killsCount parameter
|
||||||
|
const limitedKills = useMemo(() => {
|
||||||
|
if (!detailedKills || detailedKills.length === 0) return [];
|
||||||
|
return detailedKills.slice(0, killsCount);
|
||||||
|
}, [detailedKills, killsCount]);
|
||||||
|
|
||||||
|
if (!killsCount || limitedKills.length === 0 || !systemId || isLoading) return null;
|
||||||
|
|
||||||
|
// Calculate a reasonable height for the tooltip based on the number of kills
|
||||||
|
// but cap it to avoid excessively large tooltips
|
||||||
|
const maxKillsToShow = Math.min(limitedKills.length, 20);
|
||||||
|
const tooltipHeight = Math.max(200, Math.min(500, maxKillsToShow * 35));
|
||||||
|
|
||||||
const tooltipContent = (
|
const tooltipContent = (
|
||||||
<div style={{ width: '100%', minWidth: '300px', overflow: 'hidden' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
width: '400px',
|
||||||
|
height: `${tooltipHeight}px`,
|
||||||
|
maxHeight: '500px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="p-2 border-b border-stone-700 bg-stone-800 text-stone-200 font-medium">
|
||||||
|
System Kills ({limitedKills.length})
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-hidden">
|
||||||
<SystemKillsContent
|
<SystemKillsContent
|
||||||
kills={detailedKills}
|
kills={limitedKills}
|
||||||
systemNameMap={systemNameMap}
|
systemNameMap={systemNameMap}
|
||||||
onlyOneSystem={true}
|
onlyOneSystem={true}
|
||||||
autoSize={true}
|
// Don't use autoSize here as we want the virtual scroller to handle scrolling
|
||||||
limit={killsCount}
|
autoSize={false}
|
||||||
|
// We've already limited the kills to match killsCount
|
||||||
|
limit={undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -27,13 +27,12 @@ export function useKillsCounter({ realSystemId }: UseKillsCounterProps) {
|
|||||||
const filteredKills = useMemo(() => {
|
const filteredKills = useMemo(() => {
|
||||||
if (!allKills || allKills.length === 0) return [];
|
if (!allKills || allKills.length === 0) return [];
|
||||||
|
|
||||||
return [...allKills]
|
// Sort kills by time, most recent first, but don't limit the number of kills
|
||||||
.sort((a, b) => {
|
return [...allKills].sort((a, b) => {
|
||||||
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
const aTime = a.kill_time ? new Date(a.kill_time).getTime() : 0;
|
||||||
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
const bTime = b.kill_time ? new Date(b.kill_time).getTime() : 0;
|
||||||
return bTime - aTime;
|
return bTime - aTime;
|
||||||
})
|
});
|
||||||
.slice(0, 10);
|
|
||||||
}, [allKills]);
|
}, [allKills]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -14,11 +14,7 @@ interface UseSystemKillsProps {
|
|||||||
sinceHours?: number;
|
sinceHours?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineKills(
|
function combineKills(existing: DetailedKill[], incoming: DetailedKill[], sinceHours: number): DetailedKill[] {
|
||||||
existing: DetailedKill[],
|
|
||||||
incoming: DetailedKill[],
|
|
||||||
sinceHours: number
|
|
||||||
): DetailedKill[] {
|
|
||||||
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
|
const cutoff = Date.now() - sinceHours * 60 * 60 * 1000;
|
||||||
const byId: Record<string, DetailedKill> = {};
|
const byId: Record<string, DetailedKill> = {};
|
||||||
|
|
||||||
@@ -37,19 +33,15 @@ interface DetailedKillsEvent extends MapEvent<Commands> {
|
|||||||
payload: Record<string, DetailedKill[]>;
|
payload: Record<string, DetailedKill[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSystemKills({
|
export function useSystemKills({ systemId, outCommand, showAllVisible = false, sinceHours = 24 }: UseSystemKillsProps) {
|
||||||
systemId,
|
|
||||||
outCommand,
|
|
||||||
showAllVisible = false,
|
|
||||||
sinceHours = 24,
|
|
||||||
}: UseSystemKillsProps) {
|
|
||||||
const { data, update } = useMapRootState();
|
const { data, update } = useMapRootState();
|
||||||
const { detailedKills = {}, systems = [] } = data;
|
const { detailedKills = {}, systems = [] } = data;
|
||||||
const [settings] = useKillsWidgetSettings();
|
const [settings] = useKillsWidgetSettings();
|
||||||
const excludedSystems = settings.excludedSystems;
|
const excludedSystems = settings.excludedSystems;
|
||||||
|
|
||||||
const updateDetailedKills = useCallback((newKillsMap: Record<string, DetailedKill[]>) => {
|
const updateDetailedKills = useCallback(
|
||||||
update((prev) => {
|
(newKillsMap: Record<string, DetailedKill[]>) => {
|
||||||
|
update(prev => {
|
||||||
const oldKills = prev.detailedKills ?? {};
|
const oldKills = prev.detailedKills ?? {};
|
||||||
const updated = { ...oldKills };
|
const updated = { ...oldKills };
|
||||||
for (const [sid, killsArr] of Object.entries(newKillsMap)) {
|
for (const [sid, killsArr] of Object.entries(newKillsMap)) {
|
||||||
@@ -57,7 +49,9 @@ export function useSystemKills({
|
|||||||
}
|
}
|
||||||
return { ...prev, detailedKills: updated };
|
return { ...prev, detailedKills: updated };
|
||||||
}, true);
|
}, true);
|
||||||
}, [update]);
|
},
|
||||||
|
[update],
|
||||||
|
);
|
||||||
|
|
||||||
useMapEventListener((event: MapEvent<Commands>) => {
|
useMapEventListener((event: MapEvent<Commands>) => {
|
||||||
if (event.name === Commands.detailedKillsUpdated) {
|
if (event.name === Commands.detailedKillsUpdated) {
|
||||||
@@ -73,17 +67,18 @@ export function useSystemKills({
|
|||||||
|
|
||||||
const effectiveSystemIds = useMemo(() => {
|
const effectiveSystemIds = useMemo(() => {
|
||||||
if (showAllVisible) {
|
if (showAllVisible) {
|
||||||
return systems.map((s) => s.id).filter((id) => !excludedSystems.includes(Number(id)));
|
return systems.map(s => s.id).filter(id => !excludedSystems.includes(Number(id)));
|
||||||
}
|
}
|
||||||
return systems.map((s) => s.id);
|
return systems.map(s => s.id);
|
||||||
}, [systems, excludedSystems, showAllVisible]);
|
}, [systems, excludedSystems, showAllVisible]);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0);
|
const didFallbackFetch = useRef(Object.keys(detailedKills).length !== 0);
|
||||||
|
|
||||||
const mergeKillsIntoGlobal = useCallback((killsMap: Record<string, DetailedKill[]>) => {
|
const mergeKillsIntoGlobal = useCallback(
|
||||||
update((prev) => {
|
(killsMap: Record<string, DetailedKill[]>) => {
|
||||||
|
update(prev => {
|
||||||
const oldMap = prev.detailedKills ?? {};
|
const oldMap = prev.detailedKills ?? {};
|
||||||
const updated: Record<string, DetailedKill[]> = { ...oldMap };
|
const updated: Record<string, DetailedKill[]> = { ...oldMap };
|
||||||
|
|
||||||
@@ -95,9 +90,12 @@ export function useSystemKills({
|
|||||||
|
|
||||||
return { ...prev, detailedKills: updated };
|
return { ...prev, detailedKills: updated };
|
||||||
});
|
});
|
||||||
}, [update, sinceHours]);
|
},
|
||||||
|
[update, sinceHours],
|
||||||
|
);
|
||||||
|
|
||||||
const fetchKills = useCallback(async (forceFallback = false) => {
|
const fetchKills = useCallback(
|
||||||
|
async (forceFallback = false) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -131,8 +129,7 @@ export function useSystemKills({
|
|||||||
const arr = resp.kills as DetailedKill[];
|
const arr = resp.kills as DetailedKill[];
|
||||||
const sid = systemId ?? 'unknown';
|
const sid = systemId ?? 'unknown';
|
||||||
mergeKillsIntoGlobal({ [sid]: arr });
|
mergeKillsIntoGlobal({ [sid]: arr });
|
||||||
}
|
} else if (resp?.systems_kills) {
|
||||||
else if (resp?.systems_kills) {
|
|
||||||
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
|
mergeKillsIntoGlobal(resp.systems_kills as Record<string, DetailedKill[]>);
|
||||||
} else {
|
} else {
|
||||||
console.warn('[useSystemKills] Unexpected kills response =>', resp);
|
console.warn('[useSystemKills] Unexpected kills response =>', resp);
|
||||||
@@ -143,7 +140,9 @@ export function useSystemKills({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal]);
|
},
|
||||||
|
[showAllVisible, systemId, outCommand, effectiveSystemIds, sinceHours, mergeKillsIntoGlobal],
|
||||||
|
);
|
||||||
|
|
||||||
const debouncedFetchKills = useMemo(
|
const debouncedFetchKills = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -156,11 +155,11 @@ export function useSystemKills({
|
|||||||
|
|
||||||
const finalKills = useMemo(() => {
|
const finalKills = useMemo(() => {
|
||||||
if (showAllVisible) {
|
if (showAllVisible) {
|
||||||
return effectiveSystemIds.flatMap((sid) => detailedKills[sid] ?? []);
|
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||||
} else if (systemId) {
|
} else if (systemId) {
|
||||||
return detailedKills[systemId] ?? [];
|
return detailedKills[systemId] ?? [];
|
||||||
} else if (didFallbackFetch.current) {
|
} else if (didFallbackFetch.current) {
|
||||||
return effectiveSystemIds.flatMap((sid) => detailedKills[sid] ?? []);
|
return effectiveSystemIds.flatMap(sid => detailedKills[sid] ?? []);
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}, [showAllVisible, systemId, effectiveSystemIds, detailedKills]);
|
}, [showAllVisible, systemId, effectiveSystemIds, detailedKills]);
|
||||||
|
|||||||
18
lib/wanderer_app/utils/http_util.ex
Normal file
18
lib/wanderer_app/utils/http_util.ex
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
defmodule WandererApp.Utils.HttpUtil do
|
||||||
|
@moduledoc """
|
||||||
|
Utility functions for HTTP operations and error handling.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Determines if an HTTP error is retriable.
|
||||||
|
|
||||||
|
Returns `true` for common transient errors like timeouts and server errors (500, 502, 503, 504).
|
||||||
|
"""
|
||||||
|
def retriable_error?(:timeout), do: true
|
||||||
|
def retriable_error?("Unexpected status: 500"), do: true
|
||||||
|
def retriable_error?("Unexpected status: 502"), do: true
|
||||||
|
def retriable_error?("Unexpected status: 503"), do: true
|
||||||
|
def retriable_error?("Unexpected status: 504"), do: true
|
||||||
|
def retriable_error?("Request failed"), do: true
|
||||||
|
def retriable_error?(_), do: false
|
||||||
|
end
|
||||||
@@ -7,6 +7,7 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
|||||||
use Retry
|
use Retry
|
||||||
|
|
||||||
alias WandererApp.Zkb.KillsProvider.{Parser, KillsCache, ZkbApi}
|
alias WandererApp.Zkb.KillsProvider.{Parser, KillsCache, ZkbApi}
|
||||||
|
alias WandererApp.Utils.HttpUtil
|
||||||
|
|
||||||
@page_size 200
|
@page_size 200
|
||||||
@max_pages 2
|
@max_pages 2
|
||||||
@@ -190,9 +191,28 @@ defmodule WandererApp.Zkb.KillsProvider.Fetcher do
|
|||||||
defp parse_partial(_other, _cutoff_dt), do: :skip
|
defp parse_partial(_other, _cutoff_dt), do: :skip
|
||||||
|
|
||||||
defp fetch_full_killmail(k_id, k_hash) do
|
defp fetch_full_killmail(k_id, k_hash) do
|
||||||
|
retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
|
||||||
case WandererApp.Esi.get_killmail(k_id, k_hash) do
|
case WandererApp.Esi.get_killmail(k_id, k_hash) do
|
||||||
{:ok, full_km} -> {:ok, full_km}
|
{:ok, full_km} ->
|
||||||
{:error, reason} -> {:error, reason}
|
{:ok, full_km}
|
||||||
|
|
||||||
|
{:error, :timeout} ->
|
||||||
|
Logger.warning("[Fetcher] ESI get_killmail timeout => kill_id=#{k_id}, retrying...")
|
||||||
|
raise "ESI timeout, will retry"
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
Logger.warning("[Fetcher] ESI get_killmail not_found => kill_id=#{k_id}")
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
if HttpUtil.retriable_error?(reason) do
|
||||||
|
Logger.warning("[Fetcher] ESI get_killmail retriable error => kill_id=#{k_id}, reason=#{inspect(reason)}")
|
||||||
|
raise "ESI error: #{inspect(reason)}, will retry"
|
||||||
|
else
|
||||||
|
Logger.warning("[Fetcher] ESI get_killmail failed => kill_id=#{k_id}, reason=#{inspect(reason)}")
|
||||||
|
{:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
|||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
alias WandererApp.Zkb.KillsProvider.KillsCache
|
alias WandererApp.Zkb.KillsProvider.KillsCache
|
||||||
|
alias WandererApp.Utils.HttpUtil
|
||||||
|
use Retry
|
||||||
|
|
||||||
|
# Maximum retries for enrichment calls
|
||||||
|
@max_enrichment_retries 2
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Merges the 'partial' from zKB and the 'full' killmail from ESI, checks its time
|
Merges the 'partial' from zKB and the 'full' killmail from ESI, checks its time
|
||||||
@@ -254,12 +259,33 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
|||||||
nil -> km
|
nil -> km
|
||||||
0 -> km
|
0 -> km
|
||||||
eve_id ->
|
eve_id ->
|
||||||
|
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||||
case WandererApp.Esi.get_character_info(eve_id) do
|
case WandererApp.Esi.get_character_info(eve_id) do
|
||||||
{:ok, %{"name" => char_name}} ->
|
{:ok, %{"name" => char_name}} ->
|
||||||
Map.put(km, name_key, char_name)
|
{:ok, char_name}
|
||||||
|
|
||||||
_ ->
|
{:error, :timeout} ->
|
||||||
km
|
Logger.debug(fn -> "[Parser] Character info timeout, retrying => id=#{eve_id}" end)
|
||||||
|
raise "Character info timeout, will retry"
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
Logger.debug(fn -> "[Parser] Character not found => id=#{eve_id}" end)
|
||||||
|
:skip
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
if HttpUtil.retriable_error?(reason) do
|
||||||
|
Logger.debug(fn -> "[Parser] Character info retriable error => id=#{eve_id}, reason=#{inspect(reason)}" end)
|
||||||
|
raise "Character info error: #{inspect(reason)}, will retry"
|
||||||
|
else
|
||||||
|
Logger.debug(fn -> "[Parser] Character info failed => id=#{eve_id}, reason=#{inspect(reason)}" end)
|
||||||
|
:skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, char_name} -> Map.put(km, name_key, char_name)
|
||||||
|
_ -> km
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -269,18 +295,36 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
|||||||
nil -> km
|
nil -> km
|
||||||
0 -> km
|
0 -> km
|
||||||
corp_id ->
|
corp_id ->
|
||||||
|
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||||
case WandererApp.Esi.get_corporation_info(corp_id) do
|
case WandererApp.Esi.get_corporation_info(corp_id) do
|
||||||
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
|
{:ok, %{"ticker" => ticker, "name" => corp_name}} ->
|
||||||
|
{:ok, {ticker, corp_name}}
|
||||||
|
|
||||||
|
{:error, :timeout} ->
|
||||||
|
Logger.debug(fn -> "[Parser] Corporation info timeout, retrying => id=#{corp_id}" end)
|
||||||
|
raise "Corporation info timeout, will retry"
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
Logger.debug(fn -> "[Parser] Corporation not found => id=#{corp_id}" end)
|
||||||
|
:skip
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
if HttpUtil.retriable_error?(reason) do
|
||||||
|
Logger.debug(fn -> "[Parser] Corporation info retriable error => id=#{corp_id}, reason=#{inspect(reason)}" end)
|
||||||
|
raise "Corporation info error: #{inspect(reason)}, will retry"
|
||||||
|
else
|
||||||
|
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
|
||||||
|
:skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, {ticker, corp_name}} ->
|
||||||
km
|
km
|
||||||
|> Map.put(ticker_key, ticker)
|
|> Map.put(ticker_key, ticker)
|
||||||
|> Map.put(name_key, corp_name)
|
|> Map.put(name_key, corp_name)
|
||||||
|
_ -> km
|
||||||
{:error, reason} ->
|
|
||||||
Logger.warning("[Parser] Failed to fetch corp info: ID=#{corp_id}, reason=#{inspect(reason)}")
|
|
||||||
km
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
km
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -290,14 +334,36 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
|||||||
nil -> km
|
nil -> km
|
||||||
0 -> km
|
0 -> km
|
||||||
alliance_id ->
|
alliance_id ->
|
||||||
|
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||||
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
|
{:ok, %{"ticker" => alliance_ticker, "name" => alliance_name}} ->
|
||||||
|
{:ok, {alliance_ticker, alliance_name}}
|
||||||
|
|
||||||
|
{:error, :timeout} ->
|
||||||
|
Logger.debug(fn -> "[Parser] Alliance info timeout, retrying => id=#{alliance_id}" end)
|
||||||
|
raise "Alliance info timeout, will retry"
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
Logger.debug(fn -> "[Parser] Alliance not found => id=#{alliance_id}" end)
|
||||||
|
:skip
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
if HttpUtil.retriable_error?(reason) do
|
||||||
|
Logger.debug(fn -> "[Parser] Alliance info retriable error => id=#{alliance_id}, reason=#{inspect(reason)}" end)
|
||||||
|
raise "Alliance info error: #{inspect(reason)}, will retry"
|
||||||
|
else
|
||||||
|
Logger.debug(fn -> "[Parser] Alliance info failed => id=#{alliance_id}, reason=#{inspect(reason)}" end)
|
||||||
|
:skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, {alliance_ticker, alliance_name}} ->
|
||||||
km
|
km
|
||||||
|> Map.put(ticker_key, alliance_ticker)
|
|> Map.put(ticker_key, alliance_ticker)
|
||||||
|> Map.put(name_key, alliance_name)
|
|> Map.put(name_key, alliance_name)
|
||||||
|
_ -> km
|
||||||
_ ->
|
|
||||||
km
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -307,13 +373,31 @@ defmodule WandererApp.Zkb.KillsProvider.Parser do
|
|||||||
nil -> km
|
nil -> km
|
||||||
0 -> km
|
0 -> km
|
||||||
type_id ->
|
type_id ->
|
||||||
|
result = retry with: exponential_backoff(200) |> randomize() |> cap(2_000) |> expiry(10_000), rescue_only: [RuntimeError] do
|
||||||
case WandererApp.CachedInfo.get_ship_type(type_id) do
|
case WandererApp.CachedInfo.get_ship_type(type_id) do
|
||||||
{:ok, nil} -> km
|
{:ok, nil} -> :skip
|
||||||
{:ok, %{name: ship_name}} -> Map.put(km, name_key, ship_name)
|
{:ok, %{name: ship_name}} -> {:ok, ship_name}
|
||||||
{:error, reason} ->
|
{:error, :timeout} ->
|
||||||
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
|
Logger.debug(fn -> "[Parser] Ship type timeout, retrying => id=#{type_id}" end)
|
||||||
km
|
raise "Ship type timeout, will retry"
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
Logger.debug(fn -> "[Parser] Ship type not found => id=#{type_id}" end)
|
||||||
|
:skip
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
if HttpUtil.retriable_error?(reason) do
|
||||||
|
Logger.debug(fn -> "[Parser] Ship type retriable error => id=#{type_id}, reason=#{inspect(reason)}" end)
|
||||||
|
raise "Ship type error: #{inspect(reason)}, will retry"
|
||||||
|
else
|
||||||
|
Logger.warning("[Parser] Failed to fetch ship type: ID=#{type_id}, reason=#{inspect(reason)}")
|
||||||
|
:skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, ship_name} -> Map.put(km, name_key, ship_name)
|
||||||
_ -> km
|
_ -> km
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
|||||||
require Logger
|
require Logger
|
||||||
alias WandererApp.Zkb.KillsProvider.Parser
|
alias WandererApp.Zkb.KillsProvider.Parser
|
||||||
alias WandererApp.Esi
|
alias WandererApp.Esi
|
||||||
|
alias WandererApp.Utils.HttpUtil
|
||||||
|
use Retry
|
||||||
|
|
||||||
@heartbeat_interval 1_000
|
@heartbeat_interval 1_000
|
||||||
|
@max_esi_retries 3
|
||||||
|
|
||||||
# Called by `KillsProvider.handle_connect`
|
# Called by `KillsProvider.handle_connect`
|
||||||
def handle_connect(_status, _headers, %{connected: _} = state) do
|
def handle_connect(_status, _headers, %{connected: _} = state) do
|
||||||
@@ -69,17 +72,42 @@ defmodule WandererApp.Zkb.KillsProvider.Websocket do
|
|||||||
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
|
# The partial from zKillboard has killmail_id + zkb.hash, but no time/victim/attackers
|
||||||
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
|
defp parse_and_store_zkb_partial(%{"killmail_id" => kill_id, "zkb" => %{"hash" => kill_hash}} = partial) do
|
||||||
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
|
Logger.debug(fn -> "[KillsProvider.Websocket] parse_and_store_zkb_partial => kill_id=#{kill_id}" end)
|
||||||
|
|
||||||
|
result = retry with: exponential_backoff(300) |> randomize() |> cap(5_000) |> expiry(30_000), rescue_only: [RuntimeError] do
|
||||||
case Esi.get_killmail(kill_id, kill_hash) do
|
case Esi.get_killmail(kill_id, kill_hash) do
|
||||||
{:ok, full_esi_data} ->
|
{:ok, full_esi_data} ->
|
||||||
# Merge partial zKB fields (like totalValue) onto ESI data
|
# Merge partial zKB fields (like totalValue) onto ESI data
|
||||||
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
|
enriched = Map.merge(full_esi_data, %{"zkb" => partial["zkb"]})
|
||||||
Parser.parse_and_store_killmail(enriched)
|
Parser.parse_and_store_killmail(enriched)
|
||||||
|
:ok
|
||||||
|
|
||||||
|
{:error, :timeout} ->
|
||||||
|
Logger.warning("[KillsProvider.Websocket] ESI get_killmail timeout => kill_id=#{kill_id}, retrying...")
|
||||||
|
raise "ESI timeout, will retry"
|
||||||
|
|
||||||
|
{:error, :not_found} ->
|
||||||
|
Logger.warning("[KillsProvider.Websocket] ESI get_killmail not_found => kill_id=#{kill_id}")
|
||||||
|
:skip
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
|
if HttpUtil.retriable_error?(reason) do
|
||||||
|
Logger.warning("[KillsProvider.Websocket] ESI get_killmail retriable error => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||||
|
raise "ESI error: #{inspect(reason)}, will retry"
|
||||||
|
else
|
||||||
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
Logger.warning("[KillsProvider.Websocket] ESI get_killmail failed => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||||
:skip
|
:skip
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
case result do
|
||||||
|
:ok -> :ok
|
||||||
|
:skip -> :skip
|
||||||
|
{:error, reason} ->
|
||||||
|
Logger.error("[KillsProvider.Websocket] ESI get_killmail exhausted retries => kill_id=#{kill_id}, reason=#{inspect(reason)}")
|
||||||
|
:skip
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp parse_and_store_zkb_partial(_),
|
defp parse_and_store_zkb_partial(_),
|
||||||
do: :skip
|
do: :skip
|
||||||
|
|||||||
Reference in New Issue
Block a user