Управление частотой вызова метода

191
05 ноября 2021, 23:40

Пишу сервис для обращения к vk api. У них стоит ограничение на запросы - не более 3 раз в секунду на методы c user token и 20 раз в секунду на методы с group token. Мое приложение постоянно делает запросы и поэтому я решил сделать механизм ограничения. Вот мое решение с использованием spring aop:

@Slf4j
@Aspect
@Component
public class BarrierMechanics {
private final Map<Integer, BarrierExecutor> executors = new HashMap<>();
private final ExecutorService pool;
private final Environment env;
public BarrierMechanics(
        @Qualifier("barrierPool") Executor pool,
        Environment env) {
    this.pool = (ExecutorService) pool;
    this.env = env;
}

@Around("@annotation(Frequency)")
public Object proceedAsync(
        ProceedingJoinPoint pJP,
        Frequency Frequency) throws Throwable {
    int bar = Integer.parseInt(env
            .resolvePlaceholders(Frequency.expression()));
    if (executors.containsKey(bar)) {
        return executors.get(bar).proceedAsync(pJP);
    } else {
        val barExec = new BarrierExecutor(bar);
        executors.put(bar, barExec);
        return barExec.proceedAsync(pJP);
    }
}
private class BarrierExecutor {
    private final Lock locker = new ReentrantLock();
    private final int barrier;
    private int counter = 0;
    private long first = 0;
    private long last = 0;
    private BarrierExecutor(int barrier) {
        if (barrier < 2) throw new IllegalArgumentException("Barrier cannot be less 2");
        this.barrier = barrier - 1;
    }

    Object proceedAsync(
            ProceedingJoinPoint pJP) throws Throwable {
        val future = pool.submit(() -> {
            try {
                locker.lock();
                if (counter == barrier) {
                    last = currentTimeMillis();
                    long res = last - first;
                    Thread.sleep(res > 1000 ? 0: 1000 - res);
                    counter = 0;
                } else if (counter == 0) {
                    first = currentTimeMillis();
                    counter++;
                } else counter++;
                locker.unlock();
                return pJP.proceed();
            } catch (Throwable throwable) {
                if (throwable instanceof VkServerException) {
                    if (((VkServerException) throwable).getCode() == 6) {
                        locker.lock();
                        Thread.sleep(300);
                        locker.unlock();
                    }
                }
                return throwable;
            }
        });
        val result = FutureUtils.anyResult(future);
        if (result instanceof Throwable) FutureUtils.anyThrow((Throwable) result);
        return result;
    }
}

}

Запросы я делаю с помощью resttemplate. Исключения преобразую в VkServerException. Ошибка с кодом 6 - слишком много запросов в секунду. Вот bean пула:

@Bean
public Executor barrierPool() {
    return Executors.newFixedThreadPool(pS, new CustomizableThreadFactory(thNm));
}

В итоге, это решение работает почти всегда корректно и вроде бы быстро. Но все же иногда возникает ошибка too many requests. У меня есть пара вопросов по этому поводу:

1) Корректно ли мое решение с точки зрения потокобезопасности?

2) Как мне правильно обработать исключение too many request, чтобы следующие вызовы отрабатывали без него?

Answer 1

1) Mодификация Map<Integer, BarrierExecutor> executors не потокобезопасная. Можно или ConcurrentHashMap и использовать putIfAbsent, или Google Guava Cache - у него можно сконфигурировать инициализатор для отсутствующего ключа.

2) В барьере, для случая counter == barrier ты обнуляешь счётчик, но не меняешь first. При этом запрос к АПИ ты делаешь. Получается этот запрос не будет учтен в интервале [first, last].

Я бы сделал так

if (counter == barrier) {
  last = currentTimeMillis();
  long res = last - first;
  Thread.sleep(res > 1000 ? 0: 1000 - res);
  counter = 0;
} 
if (counter == 0) {
  first = currentTimeMillis();
} 
counter++;

3) Из их доков я не понял, как они делают подсчёт на своей стороне. Если скользящее окно, то твой код это не учитывает. Возможно, стоит сделать на своей стороне скользящее окно, оно будет работать и в случае, если на их стороне просто всё разбито на секундные интервалы.

READ ALSO
Ошибка &#171;The mysql extension is deprecated and will be removed in the future&#187;

Ошибка «The mysql extension is deprecated and will be removed in the future»

При использовании MySQL получаю такую ошибку:

198
Соединить 2 запроса в 1

Соединить 2 запроса в 1

Пытаюсь получить товары у которых нету фото в базе и есть фото у аналогичных товаровВ первом запросе получаем model по которому идет проверка...

194
логика при работе с псевдоклассом :hover

логика при работе с псевдоклассом :hover

Первое правило прячет список-подменюВторое - показывает подменю, если на верхний пункт меню (родитель), в котором находится подменю, наведут...

118