Retry механизмы

В OpenLayers загрузка данных карты строится вокруг источников (ol/source/*) и соответствующих загрузчиков: тайловых, растровых изображений и векторных данных. Любой сетевой сбой при загрузке ресурса приводит к ошибке на уровне конкретного элемента (тайла, изображения или набора объектов), но не останавливает работу всего слоя. Это создаёт основу для реализации механизма повторных попыток (retry), который чаще всего внедряется на уровне пользовательской логики поверх стандартных API.

Ключевой особенностью является отсутствие универсального встроенного retry-механизма: поведение повторных попыток реализуется через переопределение функций загрузки или обработку событий ошибок.


Повторные попытки для тайлов XYZ/OSM

Тайловые источники (ol/source/XYZ, ol/source/OSM, ol/source/TileImage) используют загрузку по URL, формируемому шаблоном. Каждый тайл загружается независимо, и ошибка сети приводит только к его повторной отрисовке состояния ошибки.

Механизм retry реализуется через:

  • tileLoadFunction
  • обработку событий tileloaderror
  • повторную установку src у тайла

Базовая логика загрузки выглядит как установка URL:

tile.setImage(new Image());
tile.getImage().src = url;

При этом OpenLayers не ограничивает количество повторных попыток, если логика реализована вручную.


Кастомный tileLoadFunction с retry

Основной способ внедрения повторных попыток — переопределение tileLoadFunction. Эта функция вызывается для каждого тайла при необходимости загрузки.

Ключевая идея: добавление счётчика попыток в объект изображения или через WeakMap.

import TileImage from 'ol/source/TileImage';

const maxRetries = 3;
const retryDelay = 500;

const source = new TileImage({
  url: 'https://tile.server/{z}/{x}/{y}.png',
  tileLoadFunction: function (tile, src) {
    const img = tile.getImage();
    img._retryCount = img._retryCount || 0;

    const attemptLoad = () => {
      img.src = src;
    };

    img.onl oad = () => {
      img._retryCount = 0;
    };

    img.oner ror = () => {
      if (img._retryCount < maxRetries) {
        img._retryCount++;
        setTimeout(attemptLoad, retryDelay * img._retryCount);
      }
    };

    attemptLoad();
  }
});

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


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

OpenLayers предоставляет события слоя и источника, позволяющие реагировать на ошибки загрузки тайлов:

  • tileloadstart
  • tileloadend
  • tileloaderror

Событие tileloaderror позволяет централизованно реализовать повторную загрузку без модификации tileLoadFunction.

source.on('tileloaderror', function (event) {
  const tile = event.tile;
  const image = tile.getImage();

  image._retryCount = image._retryCount || 0;

  if (image._retryCount < 2) {
    image._retryCount++;
    setTimeout(() => {
      tile.load(); 
    }, 1000);
  }
});

Метод tile.load() инициирует повторный запрос тайла через стандартный pipeline источника.


Экспоненциальный backoff для тайлов

При нестабильных сетях фиксированная задержка приводит к перегрузке сервера. Более устойчивый подход — экспоненциальная задержка:

  • 1-я попытка: 300 мс
  • 2-я попытка: 600 мс
  • 3-я попытка: 1200 мс
function getDelay(attempt) {
  return Math.min(1000 * 2 ** attempt, 8000);
}

Интеграция в обработчик ошибки:

img.oner ror = () => {
  if (img._retryCount < maxRetries) {
    const delay = getDelay(img._retryCount);
    img._retryCount++;

    setTimeout(() => {
      img.src = src;
    }, delay);
  }
};

Retry для ImageSource и Static Image слоёв

Источники изображений (ol/source/ImageStatic, ol/source/ImageWMS) используют одиночный HTTP-запрос на изображение. Ошибка загрузки приводит к полному отсутствию слоя.

Механизм retry реализуется через imageLoadFunction.

import ImageWMS from 'ol/source/ImageWMS';

const source = new ImageWMS({
  url: 'https://server/wms',
  params: { LAYERS: 'layer' },
  imageLoadFunction: function (image, src) {
    let attempts = 0;

    const load = () => {
      const img = image.getImage();
      img.src = src;
    };

    const onEr ror = () => {
      if (attempts < 3) {
        attempts++;
        setTimeout(load, 500 * attempts);
      }
    };

    const img = image.getImage();
    img.onl oad = () => {};
    img.oner ror = onError;

    load();
  }
});

Особенность данного подхода — полная замена стандартного HTTP-процесса загрузки.


Retry для векторных источников (GeoJSON, TopoJSON)

Векторные источники (ol/source/Vector) используют loader функцию. Именно здесь чаще всего реализуется retry для fetch-запросов.

import VectorSource from 'ol/source/Vector';

const source = new VectorSource({
  loader: function (extent, resolution, projection) {
    const url = 'https://server/data.geojson';
    let attempt = 0;

    const load = () => {
      fetch(url)
        .then(response => {
          if (!response.ok) throw new Error('HTTP error');
          return response.json();
        })
        .then(data => {
          source.addFeatures(source.getFormat().readFeatures(data));
        })
        .catch(() => {
          if (attempt < 3) {
            attempt++;
            setTimeout(load, 1000 * attempt);
          }
        });
    };

    load();
  }
});

Здесь retry становится частью бизнес-логики загрузки данных, а не графического слоя.


Retry через AbortController и контроль устаревших запросов

При частых изменениях extent или zoom возможны гонки запросов. Retry без отмены предыдущих запросов приводит к избыточной нагрузке.

Использование AbortController позволяет управлять устаревшими попытками:

let controller = null;

function loadWithRetry(url, attempt = 0) {
  if (controller) controller.abort();
  controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then(r => r.json())
    .then(data => {
      source.addFeatures(format.readFeatures(data));
    })
    .catch(err => {
      if (err.name === 'AbortError') return;

      if (attempt < 3) {
        setTimeout(() => loadWithRetry(url, attempt + 1), 500 * attempt);
      }
    });
}

Это предотвращает накопление устаревших retry-запросов при быстром взаимодействии с картой.


Централизация retry-логики через обёртки загрузчиков

При масштабных приложениях retry не дублируется в каждом источнике, а выносится в универсальные функции:

function withRetry(fn, maxRetries = 3, delay = 500) {
  return function (...args) {
    let attempt = 0;

    const exec = () => {
      fn(...args).catch(() => {
        if (attempt < maxRetries) {
          attempt++;
          setTimeout(exec, delay * attempt);
        }
      });
    };

    exec();
  };
}

Применение к loader:

loader: withRetry((extent) => {
  return fetch(url).then(r => r.json()).then(...);
})

Поведение при HTTP ошибках и CORS

Retry-логика должна учитывать различие типов ошибок:

  • 5xx ошибки — допустим повтор
  • 4xx ошибки — повтор обычно бессмыслен
  • CORS ошибки — повтор не изменит результат
  • timeout — основной кандидат для retry

В OpenLayers это важно, так как retry без фильтрации ошибок может привести к бесконечным циклам запросов при неверной конфигурации сервера.


Кэширование как альтернатива повторным попыткам

Во многих случаях retry заменяется или дополняется кэшированием:

  • HTTP cache (ETag, Cache-Control)
  • browser cache
  • service worker cache
  • tile cache (local storage / IndexedDB)

При наличии кэша повторные попытки становятся дешёвыми и не нагружают сервер.


Смешанные стратегии retry в тайловых слоях

В реальных приложениях применяется комбинация:

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

Такая комбинация критична для стабильной работы тайловых карт в условиях нестабильных сетей и распределённых серверов тайлов.