Hello my fellow Salesforcers far and wide!
Recently I was dealing with a pretty unique use case and wasn’t able to find much help on the web, so I had to get creative. When I was finished I felt so empowered to do so many more things in a near-real-time fashion. I even found a bug! (or at least a gap in the documentation on queueable apex) And so, I decided I should write about it so you can come to the conclusion I did much faster than I did…
Before you take the red pill and enter the rabbit hole with me, let’s discuss how I came to this discovery… I had a community use-case and needed to create community users pragmatically… no problem right? WRONG… enter mixed DML. You know it well, that nasty apex limit for security that makes your life as a developer so much more difficult sometimes… Well I have a solution! Enter Queueable Apex!!!
The Problem:
Can you provision users with automatic assigning of permission sets, public group members, collaboration group members, managed sharing records, and any other type of configuration data required in near real time? I can 🙂 using queueable apex. The main problem I had to solve, which led me to writing a post about queueable apex was this:
- I needed to allow creation and provisioning of brand new community contacts and users in a single button click, and I needed the new users to automatically come with specific permission set assignments and group assignments.
Some posts address things like the inevitable mixed DML problems faced when simply trying to programmatically create a new customer community user, as this would require a contact to be created alongside a user, something that is impossible in a single transaction in apex due to mixed DML restrictions…
However, my use-case goes a step further… I needed a new contact, a new user, new permission set assignments, AND new collaboration group members all to come from one click in real time :D! Here in lies the deeper problem… I couldn’t use @future because that only got me one more transaction deep (you cannot call another future from another future context and I needed to chain new transactions for each object required), but all of these objects need to be inserted without others… so I needed a separate transaction for the contact, the user, the permission sets, and the group members… To make things even more complicated, they have to happen in a specific order, you need the contact ID to create the Community User, you need the Community User ID to assign Permission Sets and Group Members. HOW IS THIS POSSIBLE?! Well… some would go straight to Batch Apex for the permission set and group member updates…
Batch Apex vs Queueable:
Now, a lot of developers will handle use-cases such as this by creating batch apex classes and running them often, such as once ever hour, in order to deal with the mixed DML issues received when trying to insert records along-side new users. This is perfectly acceptable, but it is important to know the pros and cons.
- Running a batch, or multiple batches every hour could cause lock contention errors for users trying to update records during the batch runs.
- Know your limits! There are a lot of considerations when using batch apex. You can only have 5 concurrent scheduled batch jobs running at a time and only 100 jobs in the flex queue. There is also a daily limit on number of batch apex execute methods per day that varies based on your user count. A lot of developers flock to batch apex because it is very useful to get around so many limitations that you will hit when trying to run logic within a single transaction, but there is no silver bullet in apex. It is always important to read through the considerations before using a new tool.
- Running a batch job could be better in a use-case where near real time is less important and the queueable logic may be invoked per every record in scenarios that would mean full batches of 200 or more records at a time. You can only add up to 50 queueable jobs to the queue in a given transaction, so if you find yourself using queueable from a trigger that may run more than 50 times in a single transaction, you may want to go with a scheduled job instead. Just 1 queueable job in a trigger will lower the number of records one can include per batch that will meet the criteria to enqueue said job to be 50 or less before hitting an error.
Consideration When Using Queueable with @future:
Now that we have considered our options, ruled out @future methods and decided that queueable is the best option because we have small numbers of records being processed per transaction and we need the permissions to assign in near-real time AND the records to insert sequentially, it is time to discuss the magic of queueable apex and some other weird behaviors I noticed in my experience thus far.
Before I go into exactly how I solved this problem I need to make a point that isn’t explicitly clear in the documentation on queueable apex:
- You CANNOT avoid mixed dml by chaining jobs out of a @future context!!!
Yes, it is true… I don’t know if this is a bug or just something not documented. We see in the docs that, “In asynchronous transactions (for example, from a batch Apex job), you can add only one job to the queue with System.enqueueJob.” Now some might say that this explains it, you can only add one job to the queue, and Shawn… you are trying to add multiple! Well… let’s think this out a bit further… you can only add one job to the queue per asynchronous transaction, but each one of the new jobs is it’s own asynchronous transaction, so as long as you only chain one job per queueable and only started with one in your @future context, it should work. This is because, “no limit is enforced on the depth of chained jobs, you can chain one job to another. You can repeat this process with each new child job to link it to a new child job.“
However, something strange happens if you try to access this infinite chain depth by starting first out of a @future method… the parent and child jobs will act as one transaction… you will see multiple jobs queued and attempted to execute, but if you try to insert permission sets in one, then group members in another, you will get a mixed DML error in the second job and both end up rolling back… This isn’t explicitly clear anywhere in the documentation or even how it should work according to the documentaion… but if you try to run this code (referenced from the previously linked article here:
@future static void createUser(String contactId, String email, String firstName, String lastName, String userName, String profileId) {
Database.DMLOptions dmo = new Database.DMLOptions();
dmo.EmailHeader.triggerUserEmail = true;
User u = new User(alias = 'standt', email=email,
emailencodingkey='UTF-8', lastname=lastname, languagelocalekey='en_US',
localesidkey='en_US', profileid = profileId, contactId=contactId,
timezonesidkey='America/Los_Angeles', username=username);
u.setOptions(dmo);
insert u;
ID jobID = System.enqueueJob(new AsyncExecutionExample(u));
}
And your example job looks like this:
public class AsyncExecutionExample implements Queueable {
User newUser = new User();
public AsyncExecutionExample (User newUser) {
this.newUser = newUser;
}
public void execute(QueueableContext context) {
PermissionSetAssignment p = new PermissionSetAssignment(AssigneeId = newUser.Id, PermissionSetId = 'examplePermissionSetId');
insert p;
ID jobID = System.enqueueJob(new AsyncExecutionExample2(newUser));
}
}
And your example 2 job looks like this:
public class AsyncExecutionExample2 implements Queueable {
User newUser = new User();
public AsyncExecutionExample2 (User newUser) {
this.newUser = newUser;
}
public void execute(QueueableContext context) {
GroupMember g = new GroupMember(UserOrGroupId = newUser.Id, GroupID = 'exampleGroupId');
insert g;
}
}
You will actually notice that your jobs both fail due to a mixed DML issue between job 1 and job 2… something that shouldn’t happen.
The Solution:
So, to get around this, DO NOT use @future at all… Insert the user in a queueable class that chains to the permission set class, that then chains to the next groupmember class, etc… and so forth. I don’t know why out of an @future context they behave as 1 transaction when it comes to mixed DML, but just something to know.
Something like the below code (you even get the benefit of being able to pass an actual sObject into the queueable class, rather than needing to pass a bunch of primitive data types for user creation as seen in the documentation.):
//FROM PREVIOUSLY LINKED CODE...
public Id createContact(Id acctId){
c.accountid=acctId;
insert c;
System.debug('successfully created test contact with Id:' + c.id);
//CALL THE USER CREATION FROM THE METHOD CREATING A CONTACT BY USING
//QUEUEABLE APEX
ID jobID = System.enqueueJob(new AsyncExecutionExample(c));
return c.id;
}
public class AsyncExecutionExample implements Queueable {
Contact passedContact = new Contact();
public AsyncExecutionExample (Contact passedContact) {
this.passedContact = passedContact;
}
public void execute(QueueableContext context) {
User u = new User(alias = 'standt', email= passedContact.email,
emailencodingkey='UTF-8', lastname=passedcontact.lastname, languagelocalekey='en_US',
localesidkey='en_US', profileid = 'exampleProfileId', contactId=contact.Id,
timezonesidkey='America/Los_Angeles', username=contact.email););
insert u;
ID jobID = System.enqueueJob(new AsyncExecutionExample2(u));
}
}
public class AsyncExecutionExample2 implements Queueable {
User newUser = new User();
public AsyncExecutionExample2 (User newUser) {
this.newUser = newUser;
}
public void execute(QueueableContext context) {
PermissionSetAssignment p = new PermissionSetAssignment(AssigneeId = newUser.Id, PermissionSetId = 'examplePermissionSetId');
insert p;
ID jobID = System.enqueueJob(new AsyncExecutionExample3(newUser));
}
}
public class AsyncExecutionExample3 implements Queueable {
User newUser = new User();
public AsyncExecutionExample3 (User newUser) {
this.newUser = newUser;
}
public void execute(QueueableContext context) {
GroupMember g = new GroupMember(UserOrGroupId = newUser.Id, GroupID = 'exampleGroupId');
insert g;
}
}
By chaining the job from the contact creation to a parent queueable to create the user, to then child jobs to create the permission set assignment and group members, we avoid mixed DML, do everything in near real time and avoid some of our issues with a solution using batchable and batch chaining instead (while accruing some of our own cons which were previously expressed).
Other Considerations:
- What happens if the contact inserts but for whatever reason the user insertion in the queued job fails? It may be good to have some try/catch logic added surrounding the child insertions in the queued jobs to go delete the contact if the user fails, etc… because we can’t utilize the standard rollbacks in separate transactions.
- What about error handling to the end user who is creating the new Contact and User? If the user fails insertion in the batch, how can we let the end user know? … This will be a good one for you to stew on and perhaps for another fun post about asynchronous error handling :).
Conclusion:
If you need to insert objects that have mixed DML considerations to deal with (In our example inserting a new customer community user along side permission sets and group assignments) consider first using queueable apex and job chaining out of the original transaction to do so. It will allow you to get the new users set up in near real time, avoid any lock contention issues by running too many batches in the background too often, and it will avoid any issues with overloading your batch job queue. Just be aware of the queueable limitations, and the strange issue I found when trying to chain queueable jobs out of an already asynchronous context that started from an @future method.
As always I want to end asking, what weird issues have you come into contact with where Queueable Apex may help?
Please provide any feedback or comments below!