Retry стратегии

Рендеринг карты в Mapbox GL JS опирается на потоковую загрузку множества внешних ресурсов: тайлы, стили, спрайты, glyph-шрифты, GeoJSON и изображения. Каждый из этих типов данных загружается асинхронно и подвержен сетевым сбоям, ограничениям API, таймаутам и временной недоступности CDN.

Основные источники нестабильности:

  • сетевые ошибки (DNS, TCP reset, packet loss)
  • HTTP 429 (rate limit)
  • HTTP 5xx (ошибки серверов Mapbox или прокси)
  • медленные соединения и таймауты
  • блокировка запросов (CORS, корпоративные сети)
  • нестабильный мобильный интернет
  • перегрузка вкладки браузера и прерывание запросов

Внутренний механизм загрузки Mapbox GL JS уже содержит базовую устойчивость, однако в сложных сценариях требуется построение собственной retry-стратегии поверх стандартного поведения.


Уровни retry-стратегий

Retry в Mapbox GL JS реализуется не на одном уровне, а распределяется по нескольким слоям:

Уровень 1: HTTP-запросы (тайлы, glyphs, sprites)

Самый критичный уровень — загрузка растровых и векторных тайлов. Здесь применяются стратегии:

  • повтор запроса при сетевой ошибке
  • экспоненциальная задержка
  • ограничение числа попыток
  • разделение retry для разных типов ресурсов

Уровень 2: API-уровень (sources)

Источники данных (sources) могут проваливаться целиком. Например:

  • vector source tileset недоступен
  • GeoJSON endpoint возвращает 500
  • MVT слой не отдается

Уровень 3: UI/рендер уровень

При отсутствии данных возможны:

  • пустые слои
  • частично отрисованные стили
  • flickering при повторной загрузке

На этом уровне retry проявляется как повторная установка source / repaint / trigger reload.


Базовая стратегия повторных попыток

Наиболее распространённая модель — экспоненциальный backoff:

  • первая попытка: сразу
  • вторая: 300–500 мс
  • третья: 1–2 сек
  • четвёртая: 4–8 сек

Добавляется jitter для предотвращения синхронных повторов у множества клиентов.

Пример реализации retry для fetch-запросов

function fetchWithRetry(url, options = {}, retries = 3, delay = 500) {
  return fetch(url, options).catch(async (err) => {
    if (retries <= 0) throw err;

    await new Promise(r => setTimeout(r, delay));

    return fetchWithRetry(url, options, retries - 1, delay * 2);
  });
}

Retry для тайлов (vector/raster sources)

В Mapbox GL JS тайлы загружаются через TileJSON и internal tile loader. Перехват возможен через transformRequest.

Использование transformRequest для контроля retry

const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/streets-v11',
  transformRequest: (url, resourceType) => {
    return {
      url,
      headers: {
        'x-retry-attempt': '1'
      }
    };
  }
});

Однако transformRequest сам по себе не реализует retry — он лишь позволяет внедрить метаданные и альтернативные endpoints.


Перехват ошибок загрузки карты

Mapbox GL JS предоставляет события уровня карты:

  • error
  • data
  • sourcedata
  • dataloading

Обработка error события

map.on('error', (e) => {
  const error = e.error;

  if (!error) return;

  if (error.status === 429) {
    console.warn('Rate limit reached, scheduling retry');
  }

  if (error.status >= 500) {
    console.warn('Server error, retry possible');
  }
});

Retry на уровне source data

Источники данных можно перезагружать через setData (GeoJSON) или removeSource/addSource (tile sources).

GeoJSON retry стратегия

async function loadGeoJsonWithRetry(map, sourceId, url, retries = 3) {
  try {
    const data = await fetchWithRetry(url, {}, retries);

    map.getSource(sourceId).setData(await data.json());
  } catch (e) {
    console.error('GeoJSON load failed permanently', e);
  }
}

Экспоненциальный backoff с jitter

Jitter снижает риск “thundering herd” при массовых повторных запросах.

function backoffDelay(attempt) {
  const base = Math.pow(2, attempt) * 100;
  const jitter = Math.random() * 100;
  return base + jitter;
}

Retry для sprite и glyph ресурсов

Sprite и glyph файлы загружаются один раз при старте стиля, но их сбой критичен.

Стратегия:

  • fallback CDN
  • повторная загрузка style
  • переключение style URL
const styles = [
  'mapbox://styles/mapbox/streets-v11',
  'mapbox://styles/mapbox/light-v10'
];

async function loadStyleWithFallback(map, index = 0) {
  if (index >= styles.length) return;

  map.setStyle(styles[index]);

  map.once('error', () => {
    loadStyleWithFallback(map, index + 1);
  });
}

AbortController и управление устаревшими запросами

При активном перемещении карты запросы устаревают. Retry без отмены приводит к перегрузке.

let controller;

function loadTiles(url) {
  if (controller) controller.abort();

  controller = new AbortController();

  return fetch(url, { signal: controller.signal })
    .catch(err => {
      if (err.name === 'AbortError') return;
      throw err;
    });
}

Ограничение параллелизма retry

Без лимитов retry превращается в лавину запросов.

Используется очередь:

class RequestQueue {
  constructor(limit = 4) {
    this.limit = limit;
    this.active = 0;
    this.queue = [];
  }

  enqueue(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.next();
    });
  }

  async next() {
    if (this.active >= this.limit || !this.queue.length) return;

    const { task, resolve, reject } = this.queue.shift();
    this.active++;

    try {
      const result = await task();
      resolve(result);
    } catch (e) {
      reject(e);
    }

    this.active--;
    this.next();
  }
}

Retry при HTTP 429 и rate limiting

Mapbox API активно использует ограничение запросов. Стандартная стратегия:

  • уважение заголовка Retry-After
  • увеличение delay
  • снижение частоты pan/zoom событий
async function handleRateLimit(response) {
  const retryAfter = response.headers.get('Retry-After');

  const delay = retryAfter
    ? parseInt(retryAfter) * 1000
    : 2000;

  await new Promise(r => setTimeout(r, delay));
}

Интеграция retry с render loop

Mapbox GL JS использует WebGL render loop. Retry может влиять на перерендеринг:

  • повторная загрузка source вызывает repaint
  • частые retries создают layout thrashing
  • нужно батчить изменения

Стратегия:

  • группировать retries
  • применять debounce на обновление source
  • избегать setState-like обновлений на каждый retry

Кэширование как альтернатива retry

Retry не всегда оптимален. Часто предпочтительнее:

  • HTTP cache headers
  • browser cache
  • service worker caching

Service Worker caching тайлов

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('tiles')) {
    event.respondWith(
      caches.match(event.request).then(cached => {
        return cached || fetch(event.request);
      })
    );
  }
});

Комбинированная стратегия устойчивости

Практически применимая модель включает:

  • retry с exponential backoff
  • jitter
  • AbortController для устаревших запросов
  • ограничение параллелизма
  • fallback источники (CDN, style)
  • кеширование
  • обработка 429 через Retry-After
  • минимизация перерисовок карты

Поведение при деградации сети

При ухудшении сети система должна переходить в режим:

  • снижение детализации тайлов
  • отключение второстепенных layers
  • уменьшение zoom-dependent requests
  • приоритет уже загруженных tiles

Это снижает нагрузку на retry-механизм и стабилизирует визуальный слой карты.