Scaling Your Compute Resources on Salesforce
Let's take a closer look at how to migrate a long-running Apex operation into a Salesforce Function, along with some general tips around the Salesforce DX suite.
Join the DZone community and get the full member experience.
Join For FreeThe Salesforce development platform offers many powerful features to provide your team with apps that can unlock new workflows. For many years, the platform has run on a trifecta of technologies: Visualforce (to view the data), Apex (to handle the data), and Salesforce itself (to store the data).
Apex is like many other object-oriented languages, and it has a Java-like syntax. However, Apex runs directly on Salesforce servers, allowing developers to build and deploy an app without worrying about where to host it or how to secure their data.
The Challenge of Apex and Long-Running Commands
Applications built on Salesforce run on its multitenant architecture, so server resources like CPU and memory are shared among different organizations. With shared resources, however, wouldn’t it be possible for the code for one org’s app to consume so much processor time and memory space that it affects other users in that same org? An example of this could be a command that takes several minutes to complete.
Salesforce prevents situations like this from occurring by restricting how apps on its platform run. If any app has the potential to interfere with other users, it is restricted from completing its command execution. Long-running programs can potentially interrupt other work by hogging the limited resources available.
If so, how might Salesforce developers build apps that execute long-running commands which can scale with user growth while not impacting performance?
Salesforce Functions to the Rescue
Enter Salesforce Functions, which are small programs written in JavaScript, TypeScript, or Java and deployed onto Salesforce’s servers. Functions can perform the resource-intensive work and be invoked directly from Apex. When the Salesforce Function completes the request, it can send its results back to the originating Apex application, which then performs any additional tasks on those results.
Salesforce Functions run in their own container and don’t affect other tenants on the Salesforce platform. The memory and CPU limits are much higher, allowing you to execute longer running and more complex tasks. Because Salesforce Functions run on the Salesforce platform, security is baked in; there’s no risk of privacy loss as everything runs within the same trust boundary.
In this article, we’ll take a closer look at how to migrate a long-running Apex operation into a Salesforce Function, along with some general tips around the Salesforce DX suite.
Prerequisites
This article will require you to have some understanding of both Apex and TypeScript, but don’t worry if you’re not an expert. We’ll go through every piece of code to explain what is happening.
Although the completed version of our code is available as a GitHub repository, you may also want to download some of the CLI tools which Salesforce provides to follow along (or for your own future projects).
- First, install the sfdx CLI, which is a tool that helps simplify common operations for Salesforce developers when building applications.
- It’s important that whatever you build and deploy doesn’t affect your “real” production Salesforce organization. So, create a separate scratch org that this app can interact with. To get your own scratch org, sign up for a free Developer Edition account.
- To use Salesforce Functions, you’ll need to register for a Salesforce Functions trial by contacting your Salesforce account executive.
- Finally, you can also install the Salesforce VS Code extension to make deploying your code a little bit easier. This step is optional.
Touring the Existing Code
Open up the file at force-app/main/default/classes/Dogshow.cls
and take a look at its contents:
public class Dogshow {
public static void updateAccounts() {
// Get the 5 oldest accounts
Account[] accounts = [SELECT Id, Description FROM Account ORDER BY CreatedDate ASC LIMIT 5];
// Set HTTP request
HttpRequest req = new HttpRequest();
req.setEndpoint('https://dog.ceo/api/breeds/image/random');
req.setMethod('GET');
// Create a new HTTP object to send the request object
Http http = new Http();
HTTPResponse res = http.send(req);
String body = res.getBody();
Map<String, String> m = (Map<String, String>) JSON.deserialize(jsonStr, Map<String, String>.class);
String dogUrl = m.get('message');
// loop through accounts and update the Description field
for (Account acct : oldAccounts) {
acct.Description += ' And their favorite dog can be found at ' + dogUrl;
}
// save the change you made
update accounts;
}
}
These 30 lines perform a fairly simple function:
- It grabs the five oldest Salesforce accounts using SOQL.
- It issues an HTTP request to https://dog.ceo/dog-api/, which is a site that returns a JSON object with a random URL link to a very cute dog.
- The Apex code then updates the description for those five accounts to indicate that that dog is their favorite.
This code is fairly innocuous, but it introduces two situations that present some real problems:
- When issuing an HTTP request, several factors can affect your program. For example, the server you issue requests to could be overloaded and slow to respond. While your own code may be performant, you’re at the mercy of anything outside of your control, such as an external data source.
- Although we only iterate on five accounts here, what if we built an app that needed to iterate over 500 or 5,000? Looping through them one by one would be a slow, albeit unavoidable, process.
This is an opportunity for a Salesforce Function to take over some of this work. Our Apex class can provide a list of accounts to update to a Salesforce Function. That Salesforce Function can then issue HTTP requests and update the accounts.
Before seeing what that full migration might look like, let’s first write that Salesforce Function.
Developing the Salesforce Function
Using the sfdx CLI, we can easily create a new Salesforce Function in TypeScript using a single command:
$ sf generate function -n dogshowfunction -l typescript
This creates a set of files under functions/dogshowfunction
. The most important of these is index.ts
, which is where our main Salesforce Function code will reside. However, the other files are also important, dealing with testing and linting code, as well as defining TypeScript generation and the Salesforce deployment process.
Let’s focus on index.ts
. In this file, you’ll note that there’s one function exported, and it takes three parameters:
event
: This describes the payload of data that is coming in from your Apex code.context
: This contains the authorization logic necessary to communicate with Salesforce.logger
: This is a simple logger that also integrates with Salesforce.
The template code which the CLI generates shows how powerful Salesforce Functions are:
- They can accept any JSON data sent over from Apex.
- They can work directly with Salesforce data.
- They integrate directly with the platform in such a way as to handle all the authentication and security for you.
Best of all, since this particular function runs on Node.js, you can install and use any NPM package to supplement your code. Let’s do that right now by installing node-fetch to issue our HTTP request:
$ npm i node-fetch
Our Salesforce Function will be responsible for issuing our HTTP requests and updating our five accounts. To implement that functionality, our function might look something like this:
export default async function execute(event: InvocationEvent<any>, context: Context, logger: Logger): Promise<RecordQueryResult> {
const accounts = event.data.accounts;
accounts.forEach(async (account) => {
const response = await fetch('https://dog.ceo/api/breeds/image/random');
const data = await response.json();
const message = ` And their favorite dog is ${data.message}`
const recordForUpdate = {
type: 'Account',
fields: {
id: account.id,
Description: `${account.Description} ${message}`
}
}
context.org.dataApi.updateRecord(recordForUpdate);
});
}
Let’s break down what’s going on in the code snippet above.
Our event argument is essentially a JSON object which we will define in our Apex code. Although this JSON object doesn’t exist yet, we can assume that it will have the id
and Description
of the account we want to update, based on the behavior of our previous Apex code.
From here, it’s important to note that the context argument is essentially an instantiation of the Node.js SDK for Salesforce Functions. Since we can assume that this account data is provided as an array, we can simply iterate through each item, plugging in the record data as part of our update command.
Migrate the Apex Code
With our Salesforce Function defined, we can now replace the previous Apex logic with a call to this new function. Issuing a call to a Salesforce Function is surprisingly simple: We only need to know the name of our function and provide the data we want to send it, as shown below:
public class Dogshow {
public static void updateAccounts() {
// Get the 5 oldest accounts
Account[] accounts = [SELECT Id, Description FROM Account ORDER BY CreatedDate ASC LIMIT 5];
// Set up the function
functions.Function dogshowFunction = functions.Function.get('ApexToSalesforceFunctionsMigration.dogshowfunction');
// Create a list to hold the record data
List<Map<String, String>> jsonObj = new List<Map<String, String>>();
for (Account account : accounts) {
Map<String, Object> obj = new Map<String, Object>();
obj.put('id', account.Id);
obj.put('Description', account.Description);
// Add an object to the record list
jsonObj.add(obj);
}
// Send the record list to the function
functions.FunctionInvocation invocation = dogshowFunction.invoke(JSON.Serialize(jsonObj));
// if the function had a return value, it would be available here
invocation.getResponse();
}
}
Just like that, we’ve taken the long-running operation out of our Apex code and offloaded it to a Salesforce Function. Now, we can be certain that our operation will continue to run even though it uses resources more heavily.
The implications of Salesforce Functions cannot be overstated. With Salesforce Functions handling our need for time-consuming, resource-heavy operations, apps built on the Salesforce platform now have much more potential for scaling up seamlessly.
Conclusion
Salesforce Functions provide a way to improve workflows while building effective and durable projects. Without the memory and CPU limits you would experience in Apex, you can leverage Salesforce Functions for the execution of long-running commands, bringing scalability without a negative impact on performance.
Published at DZone with permission of Michael Bogan. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments