Пример использования

  1. После запуска триггеров вставки Queueable
  2. Очередь execute() соревнуется за один и тот же ресурс, использует циклическую блокировку для ожидания доступности ресурса. Многие очереди могут быть запущены одновременно.
  3. Исключение возникает при превышении количества повторных попыток в спин-блокировке

Спин-блокировка (спасибо за это @sfdcfox)

public  class LocksServiceImpl implements ILocksService {    
    /**
     * acquire - Obtain a lock that prevents other transactions from executing
     */

    public void acquire(String lockItem) {

        Integer retryCount = 0;
        Exception error;

        while (retryCount < LocksService.MAX_RETRIES) {
            try {
                //  FOR UPDATE will pause this Txn until lock is freed by other transaction
                Mutex__c[] mutexes = MutexesSelector.newInstance().selectForUpdateByItem(new Set<String> {lockItem});
                if (mutexes.isEmpty()) {
                    insert new Mutex__c (Lockable_Item__c = lockItem);
                }
                return;     // this Txn now has the lock on lockItem
            }
            catch (Exception e) {
                if (Util.isTransientException(e)) { // UNABLE_TO_LOCK_ROW
                    error = e;
                    retryCount++;
                }
                else {
                    error = e;
                    break;
                }
            }
        }
        throw new LocksService.MutexException('Unable to obtain a transaction lock on ' + lockItem +
                ' retries:' + retryCount + ' v. maxRetries:' + LocksService.MAX_RETRIES + ' exception:' + Util.showException(error));
    }

}

Вызов

  • В большинстве случаев это работает нормально, но иногда скорость вращения превышает MAX_RETRIES и выдает исключение. Я хотел бы реализовать некоторую форму логики отсрочки, чтобы отложить повторную попытку спин-блокировки, а не сразу повторять SELECT ..FOR UPDATE
  • На самом деле, чем выше параллелизм очередей, тем больше вероятность того, что это произойдет.
  • Я рассмотрел несколько многообещающих алгоритмов отсрочки, таких как exponential и jitter.

Но как изменить задержку наиболее эффективным способом и при этом получить наилучшую производительность?

Параметры

  1. Сон ЦП в течение n секунд - не так уж много, как ограничение в 10 секунд ЦП на Txn, и я не хочу использовать этот предел во время блокировки вращения
  2. Запрос SOQL n записей (скажем, самых старых n контактов - у нас их 10E6+) - не использует ЦП, но сжигает строки SOQL, которые могут понадобиться транзакции
  3. Циклическая блокировка более 10 раз (текущий максимальный уровень повторных попыток) – не помогает, если одновременно выполняется несколько очередей – все они циклически блокируются одновременно, и у всех одновременно заканчиваются повторные попытки.
  4. Задержите (используя #1 или #2) выполнение System.enqueueJob в триггере после вставки. Может быть; триггер легкий, но я не могу контролировать, когда SFDC запускает очереди, поэтому конкуренция между очередями все еще может происходить.
  5. Объедините все объекты в очередь с помощью пользовательского шаблона объекта, такого как асинхронный шаблон Дэна Эпплмана (это мой запасной вариант, поскольку этот шаблон реализован в нашей организации).

Как я могу сделать что-то с переменной стоимостью (в прошедшем времени), которая потребляет наименьшее количество ограничений SFDC?

5
cropredy 31 Май 2019 в 00:20

2 ответа

Лучший ответ

Я просто поставлю задание в очередь, если вы нажмете максимальное количество попыток:

try {
  LocksServiceImpl.acquire(lock);
} catch(LocksService.MutexException e) {
  System.enqueueJob(this);
  return;
}

На этом этапе ваша работа будет повторена позже. Предполагается, что для связанных заданий должна быть встроенная задержка, поэтому это должно обеспечить задержку, которую вы ищете.

2
sfdcfox 30 Май 2019 в 21:47
Может быть. Я забыл о встроенной отсрочке для цепочек очередей, которая составляет что-то вроде 1-1-3-60-60-... секунд, насколько я помню. Однако, если многие параллельные очереди достигают максимального количества повторных попыток одновременно, приведенная выше логика повтора просто запускает параллельные дочерние очереди, которые снова запускаются одновременно и все снова конкурируют за один и тот же ресурс до бесконечности. Следовательно, ищем переменную отсрочку (возможно, в этом случае я мог бы сделать переменный сон процессора перед постановкой в ​​очередь)
 – 
cropredy
30 Май 2019 в 23:01
Он должен быть экспоненциальным, я думаю, 1-2-4-8-16-32-64 (повторяю). Кроме того, этот ответ был направлен только на ваш прямой вопрос. В зависимости от ситуации я мог бы просто записывать записи в пользовательский объект для постановки в очередь, а затем обрабатывать их последовательно с помощью пакетного процесса. Конечно, не зная большего, сложно сказать, но есть альтернативы.
 – 
sfdcfox
30 Май 2019 в 23:10
Да - сериализация через пользовательский объект - это мой запасной вариант, но я написал ОП, чтобы узнать, есть ли у кого-нибудь в сообществе идея получше
 – 
cropredy
30 Май 2019 в 23:13

Хотя это и не ответ сам по себе, я хотел опубликовать некоторую статистику производительности, основанную на стресс-тесте с использованием спин-блокировки и резервной стратегии спин-блокировки, как ответил @sfdcfox. Это не критика ответа, который был хорошим, а скорее предостерегающий рассказ, если кто-то пойдет по этому пути. YMMV

Тест :

  • 50 заданий в очереди (System.enqueueJob(..)), каждое из которых запущено в одной транзакции стресс-тестирования.
  • Каждое задание ставится в очередь на одну и ту же спин-блокировку, то есть все они конкурируют друг с другом.
  • Спин-блокировка делает Select for Update и пытается 10 раз, прежде чем отказаться
  • Каждое задание выполняет одинаковый объем работы: (десериализует 1000 пользовательских Json, а затем создает/вставляет 1000 Asset). То есть работа "предметная".

Результат :

  • 4 из 50 очередей были завершены быстро, а остальные были повторно поставлены в очередь с помощью решения отсрочки спин-блокировки в другую очередь. Затем SFDC перепланирует эти очереди (с задержкой до 1 минуты), и они (46) снова соревнуются друг с другом. Это повторяется со временем, пока все они не завершатся.
  • Общее время, затраченное на прохождение 50 первоначальных потоков, каждый из которых пытается вставить 1000 ресурсов, = 26 минут. Триста девять (309) повторных заданий, находящихся в очереди (!)

< Сильный > Анализ

  • YMMV
  • Пакетное задание почти наверняка будет быстрее
  • Введение некоторой переменной задержки джиттера между каждой повторной попыткой блокировки вращения, вероятно, улучшит ситуацию, но, как заявил OP, неясно, какой будет хорошая задержка, которая не потребляет дефицитный ресурс SFDC.
2
cropredy 7 Июн 2019 в 02:36