Multi-threading and context sharing in Salesforce.

Multi-threading and context sharing in Salesforce.

Столкнулся с интересной ситуацией. Может кто знает решения на SF и подскажет.

У меня есть Email Service на который приходят emails. Из этих email создаются записи в базе. Но записи в базе должны создаваться в некоторой зависимости от других. Простой пример - одна запись помечается как new, а вторая как duplicate если похожая уже есть в базе.

Стал замечать что это не работает. И в базе переодически появляются два new, но созданные в одно время. Получается что приходят два emails, стартует два потока (с apex логикой и проверкой на дубликаты), которые заканчиваются insert записи в базу. И получается что оба потока не знаю ничего друг о друге и не видят в базе пока другие записи и добросовестно создают две записи помеченные как new.

Может есть какие-то инструменты в SF для того чтобы шарить контекст между этими потоками? Чтобы потоки знали друг о друге до того как записи попадут в базу?

Есть вариант вынести эту логику в scheduled batch (сделать очередь). Который будет стартовать каждые 10 мин и обрабатывать записи по очереди, но пока этот вариант клиент забанил, аргументируя тем что должно работать сразу (в синхронном контексте).

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

А зачем создавать дубли? Не проще настроить стандартные правила дупликации и запретить создавать дубли.
Либо вынести проверку на дубли в after insert триггер.

Я для примера про дубли чтобы было понятно. Там логика вообще не про дубли, и намного сложнее чтобы описать.
Но смысл в том что мне надо как-то заставить Email Service не выполнялись параллельно или хотя бы хоть как-то знали что другой Email Service работает в это время.

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

Но проблема в том что в Salesforce нет возможности шарить контекст между потоками (как в других языках) или я про такую возможность не знаю.

РАБОТАЕТ
1.EMAIL------CHECK_FOR_OTHER_RECORDS_IN_DB------INSERT
2.----------------------------------------------------------------------------EMAIL------CHECK_FOR_OTHER_RECORDS_IN_DB------INSERT

НЕ РАБОТАЕТ
1.EMAIL_SERVICE---------CHECK_FOR_OTHER_RECORDS_IN_DB-----------INSERT
2.--------EMAIL_SERVICE---------CHECK_FOR_OTHER_RECORDS_IN_DB------------INSERT

Это две варианта с двумя Emails пришедших на Email Service на TIMELINE
В варианте 1. все красиво. первый Email уже отработал и запись в базе есть и CHECK_FOR_OTHER_RECORDS_IN_DB ее найдет
В варианте 2. все плохо. Вроде запись есть, но она еще в процессе обработки и базу не попала. Но второй поток про это не знает и CHECK_FOR_OTHER_RECORDS_IN_DB показывает что записей нет.

Гугл тоже не помог, поэтому предполагаю что execution context в Salesforce (как он правильно называется) не шарятся между собой. Наверное все-таки отдельный scheduled batch который будет обрабатывать записи по очереди это единственный вариант

Тебе нужно чтобы твоя операция на SF стороне была идемпотентной. Глянь тут пару вариантов, может какой-то из них тебе подойдет. https://developer.salesforce.com/blogs/engineering/2013/01/implementing-idempotent-operations-with-salesforce.html

А может сделать асинхронность через Queueable интерфейс? И шаг 2 делать если только шаг 1 завершен.

Rustam Muhametdinov
Тебе нужно чтобы твоя операция на SF стороне была идемпотентной. Глянь тут пару вариантов, может какой-то из них тебе подойдет. https://developer.salesforce.com/blogs/engineering/2013/01/implementing-idempotent-operations-with-salesforce.html

Рустам, спасибо за ссылку. Глянул бегло, про мой случай, буду вчитываться и думать как это дело подвести под мой случай.

akr0bat
А может сделать асинхронность через Queueable интерфейс? И шаг 2 делать если только шаг 1 завершен.

Пробовал сделать что-то похожее. Но через Batch. В общем сделал батч (self-called batch) который вызывается из одного контекста, а во втором проверяется запущен ли он и не запускается. А сам батч уже обрабатывает записи из первого и второго потока последовательно. Думал что сработает а нифуя. Опять получилась такая же фигня - первый поток ищет запущенный батч и не найдя его запускает, второй поток работающий параллельно тоже ищет запущенный батч и тоже его не находит и запускает. Получается два одновременно запущенных батча которые параллельно работают по одим и тем же записям. Короче тот же гемор только сбоку.

Блин, помню где-то была такая фигня как lock на уровне базы.
Можно ли к примеру поставить лок на запись, а в другом потоке проверить, залочена запись или нет?

Еще такой момент уточнить.

Когда работаем с батчем и возвращаем SOQL через Database.getQueryLocator(query) в start методе.

в execute мы получаем записи с полями в которых значения на время выполнения execute метода или start?

Вот к примеру батч работает в течении часа. Записи которые попадают в execute они имеют значения на момент начала часа и все изменения в базе после этого уже игнорятся или каждый execute получает актуальные записи на момент вызова этого метода?

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

Не совсем понимаю почему все же в триггере не сделать проверку и если что выбрасывать exception

wilder, ты просто предлагаешь сделать.
Как видишь из timeline тоже не будет работать

1.EMAIL_SERVICE---------INSERT----------CHECK_FOR_OTHER_RECORDS_IN_DB
2.--------EMAIL_SERVICE---------INSERT---------CHECK_FOR_OTHER_RECORDS_IN_DB

Вернее будет работать исходя из моей картинки

но все равно смысл вопроса свести параллельные контексты в последовательный. А пока кроме как отдельный scheduler+batch больше в голову ничего не приходит.

Dmitry Shnyrev
но все равно смысл вопроса свести параллельные контексты в последовательный. А пока кроме как отдельный scheduler+batch больше в голову ничего не приходит.

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

Пока не вижу проблем в логике.

Да так и хочу сделать.
Да клиент пока не хочет чтобы было батчем, а хочет чтобы сразу на каждый пришедший Email все работало. Мол задержка будет

Но вроде когда я последний раз сталкивался с шедулером вроде минимально можно было за 10 минут его задавать и то программно. Если каждую минуту можно, то наверное уговорю. Что-то поменялось в запуске батчей? Насколько я понимаю делается scheduler на минимальное время (10 мин) который запускает батч и просто проверяет во время следующего срабатываения не выполняется ли он еще чтобы не запустить другой. Что-то поменялось в SF?

А, еще был аргумент от клиента - не засирать Apex Jobs list нашими schedulers и batches. Мол если делать свои то засрем все выдачу у клиента и будут ругаться.

Скедулер запускай и убивай сам. Каждую минуту. И будет тебе счастье.

В этом случае у тебя всегда будет только один батч и ничего проверять будет не нужно.

Типа email могут приходить и 1000 за секунду и 1 за сутки. А scheduler каждую минуту будет запускаться.

Ты имеешь в виду self-called batch. Который из finish метода стартует самого себя с задержкой в минуту?

Dmitry Shnyrev
Ты имеешь в виду self-called batch. Который из finish метода стартует самого себя с задержкой в минуту?

Батч не будет запускать себя сам. Он будет запускать скедуллер. И скедуллер будет проверять нужно ли запускать батч.

Вот типа это из finish вызывать
https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_system.htm

Да, от scheduler избавимся. Но все равно в списке Apex Jobs сам батч будет висеть и его будет много.

Мой последний вариант который я сделал. Был почти такой батч. Он должен был стартовать при приходе первого Email и работать циклично (вызывать самого себя) пока есть в базе необработанные записи. Все последующие приходящие Email просто видят что батч в работе и просто создают записи помеченные на обработку. Батч крутится в цикле пока видит записи на обработку и когда они заканчиваются умирает. Следующий пришедший Email запускает батч сново. Таким образом батч не должен молотить впустую.
НО как показал тестовый орг - были случаи когда приходило куча email в короткий промежуток и запускался не один батч, а несколько. И они начинали параллельно по тем же записям работать все ломая. Происходило это потому что в параллельных контекстах происходила проверка на запущенный батч и его запуск. Так как запускались одновременно то параллельные контексты не знал что они делают это одновременно.

Если запустить батч один раз и не убивать его когда закончатся записи на обработку, то все ок. Любой прищедший Email увидит что батч работает и не будет его запускать. Все прекрасно. Но тогда батч будет тарабанить вхолостую и засерать логи.

wilder
Батч не будет запускать себя сам. Он будет запускать скедуллер. И скедуллер будет проверять нужно ли запускать батч.

Разницы в принципе нет. Шедулер будет проверять или сам батч будет проверять запускать самого себя или нет
Сорри ссылку непонятную скинул выше. Там просто по ней есть

scheduleBatch(batchable, jobName, minutesFromNow)

И если запихнуть это в finish метод. То тот же самый scheduler получится

Думаю еще как вариант добавить проверку внутри батча на самого себя. И если запущено больше 1-го батча то убивать второй.

НО опять же ситуация хрен пойми. Запустились параллельно два батча и оба одновременно увидели что есть 2 батча в очереди и оба убили друг друга Тут тоже надо какой-то механизм придумывать. И самое проблемное что ХЗ как протестировать. Когда на тестовом орге делаешь - все гуд. Работает как часы. Создается, убивается. Прям красота. Заливаешь на Прод и пиздец - одни косяки из-за параллельного контекста. И сидишь на пальцах теорию раскладываешь откуда ноги ростут.

Они никак не могут запуститься параллельно!

wilder
Они никак не могут запуститься параллельно!

Ну я хз тогда как еще можно объяснить то что у меня происходит с логикой.
Я вижу в Apex Jobs списке два Batch созданных с разницей в секунду, при том что они сами по себе могу выполняться минимум пару минут. То есть это точно значит что они созданы параллельно, потому что иначе если хотя бы один из них уже в очереди присутствовал, то другие из-за проверки не могут создаться. Проверка на запущенный батч точно работает, проверял.

А вот вопрос могут ли выполняться батчи параллельно это согласен, вопрос. То что методы в execute вызываются последовательно это понятно, но если запустить два батча. Они сами по себе стартанут параллельно или последовательно?

А если использовать unique поле? Типа externalID. Тогда при insert записи, если такой externalID существует, то Exception дожен появиться.

Ok. Полная логика

Emailservice работает на создание записей.

1 раз в минуту запускается скедуллер, который проверяет нужно ли запускать батч. И убивает сам себе.он так же проверяет не запущен ли батч.

Если нужно запускать значит запускаем батч. В финише батча запускаем скедулер

Если не нужно запускать батч. Перезапускаем скедуллер.

в сэйлсфорсе полно косяков такого типа и избавиться от рассинхрона очень сложно

я встречал ситуацию когда массовые API колы в одну и туже секунду умудрялись создавать несколько кастом сеттинг рекордов с одинаковым именем, что невозможно на уровне сэйлсфорса. Так что я не верю что валидейшен рул или дедуп логика поможет в 100% случаев.

Андрей
Так что я не верю что валидейшен рул или дедуп логика поможет в 100% случаев.

Ну хоть кто-то вкурил ситуацию

Всем спасибо за советы! Буду делать как описал wilder. Танцы с отдельной очередью из шедулера и батча. Потому что все остальные попытки связать контекст через базу нихрена не работает.

"стартует два потока (с apex логикой и проверкой на дубликаты), которые заканчиваются insert записи в базу"

проверку состояния базы логичней было бы делать в after insert контексте триггера - тогда мы будем уверены что смотрим на базу в актуальном состоянии

Кому может пригодится. Игрался с Platform Events.

Можно замутить такую штуку. Если у вас к примеру массово дергается Email или WebService Service и каждое событие обрабатывается в своей Execution Context изолировано, а хочется сделать bulk обработку в одном Execution Context (или совсем простыми словами объеденить кучу маленьких триггеров в один большой), то можно такую штуку замутить

В триггеере создавать Platform Event и публиковать его. А на Platform Event сделать триггер который должен выполнять bulk обработку.

В итоге 100 отдельных триггеров на объекте опубликуют 100 Platform Events. А в сам триггер который висит на Platform Event уже попадет пачка Events. И вместо 100 работающих параллельно триггеров сработает 2-3 триггера в которые попадут 40+30+30 записей.

Опять же в моем случае не сильно помогло, потому что Execution Context на Platform Event Trigger тоже асинхронные и срабатывают практически одновременно. То есть сделать так чтобы сработал один контекст и другой выполнялся по его окончанию нельзя. И контролировать количество объединенных в один триггер Platform Events тоже нельзя, то есть может что в один триггер придет 100 Events (может и 2000 прийти, это лимит), а может и 100 отдельных триггеров по одному Event - это уже решает платформа.

Я к примеру тестировал так - слал на Email Service 10 Emails и видел как срабатывают 10 триггеров на Email объект из которых я публиковал Email_Event__e, а на Email_Event__e повесил триггер и видел как он срабатывает 2-3 раза в вместо 10 получая на вход сразу пачку Email_Event__e.

Интересная штука. Можно найти полезные применения.

Rustam Muhametdinov
Тебе нужно чтобы твоя операция на SF стороне была идемпотентной. Глянь тут пару вариантов, может какой-то из них тебе подойдет. https://developer.salesforce.com/blogs/engineering/2013/01/implementing-idempotent-operations-with-salesforce.html

Рустам, спасибо за ссылку. Почерпнул оттуда идею про объект с уникальным полем и применил его в своем решении с кое-какими доделками. Как все-такие полезно иногда задавать на форуме глупые вопросы и получать умные ответ !

Interesting information? Help us, post link to social media..