Salesforce, как любая уважающая себя мультитенант платформа, очень серьёзно относится к вопросу распределения ресурсов между пользователями.
С этим связано обилие лимитов, которыми Salesforce-разработчики пугают друг друга, сидя вечером у костра и рассказывая страшные истории о неуспешных транзакциях.
В свете костра строгих лимитов на использование ресурсов платформы, достаточно проблемной операцией является выполнение вэб запросов с длительным ожиданием ответа. Лимит составляет 10 синхронных вызовов длительностью более 5ти секунд каждый.
Ведь пока наш текущий поток ожидает ответ от стороннего сервиса, занятые им ресурсы не могут быть освобождены и использованы на что-нибудь более полезное… Или могут?
Для решения (а лучше - избежания) данной проблемы Salesforce предоставляет возможность совершать асинхронные вызовы, которые не требуют от вызывающего потока блокировки ресурсов на время ожидания ответа от удалённого сервера. Такой асинхронный вызов называется сontinuation.
Объект Continuation содержит в себе информацию о реквесте и респонсе. Можно сказать, что работа с ним разделена на две смысловые части:
- подготовка информации для запроса и выполнение запроса.
- получение ответа, его обработка, работа с данными, обновление страницы.
Salesforce использует отдельный сервер для выполнения таких запросов, что позволяет нам освободить ресурсы вызывающего потока, сохранить информацию о транзакции и восстановить её уже после получения ответа.
1.) Пользователь совершает действие на VF странице, которым инициирует вызов стороннего сервиса.
2.) Запрос передаётся Continuation серверу.
3.) Выполнение контекста страницы приостанавливается до получения ответа от сервера.
4-7.) Continuation сервер посылает запрос и получает ответ от удалённого сервера.
8.) Continuation сервер передаёт полученный ответ.
9.) Ответ возвращается на страницу, приостановленный контекст страницы восстанавливается.
Таким образом, даже если сотни пользователей одной организации совершают вызовы стороннего сервиса (нажимая соответствующую кнопку на VF странице, например), они не будут блокировать ни друг друга, ни организацию.
Рассмотрим на примере выдуманной прямо сейчас задачи.
На моей дэв орге осталась ссылка на удалённый сервис https://ionic2-realty-rest-demo.herokuapp.com после прохождения какого-то из модулей на трэйлхэде. Туда и постучимся. Допустим, нужно написать вижуалфорс страницу, куда можно будет ввести айди аккаунта, нажать на кнопку Start Request и увидеть на странице ответ от сервера. На аккаунте нужно добавить чекбокс, который будет выставляться в true при обработке коллбэка (для аккаунта, айди которого мы ввели). Сервис возвращает html-ину, но для нас это сейчас и не важно.
Страница:
<apex:page controller="ContinuationController" showChat="false" showHeader="false">
<apex:form>
<apex:inputText value="{!accountId}"/>
<apex:commandButton action="{!startRequest}"
value="Start Request" reRender="result"/>
</apex:form>
<apex:outputText id="result" value="{!result}"/>
</apex:page>
Контроллер:
public with sharing class ContinuationController {
public String requestLabel;
public String result {get;set;}
public String accountId {get;set;}
public ContinuationController(){
accountId = '';
result = '';
}
public Object startRequest() {
Continuation con = new Continuation(40);// конструктор Continuation объекта принимает таймаут в секундах (до 120)
con.continuationMethod='processResponse'; // указываем, какой метод будет обрабатывать коллбэк
HttpRequest req = new HttpRequest();
req.setMethod('GET');
req.setEndpoint('callout:ionicRealty');// named credentials
this.requestLabel = con.addHttpRequest(req);// передаём ему обычный реквест, взамен получаем его label. В одном Continuation объекте может выполняться до 3х реквестов, label нужен, чтобы потом обратиться к нужному....
return con; // возвращаем continuation
}
public Object processResponse() {
HttpResponse response = Continuation.getResponse(this.requestLabel);// ... вот так
this.result = response.getBody();// выведем результат на страницу
if(String.isNotBlank(this.result)){
Account acc = [SELECT isRealtyChecked__c FROM Account WHERE Id = :this.accountId];
acc.isRealtyChecked__c = true;
update acc;
}
return null; // возвращаем null, чтобы отререндерить текущую страницу
}
}
Пример бессмысленный и беспощадный, впрочем, как обычно. Также стоит обратить внимание на то, что DML-операции не могут совершаться в методе, где формируется Continuation объект, а только в методе, который обрабатывает ответ. Что, в принципе, логично, так как полученную информацию часто нужно где-нибудь сохранить