apex-enterprise-patterns / fflib-apex-common

Common Apex Library supporting Apex Enterprise Patterns and much more!
BSD 3-Clause "New" or "Revised" License
911 stars 517 forks source link

Queueable + UnitOfWork: System.SerializationException: Not Serializable: com/salesforce/api/fast/List$$lcom/salesforce/api/Messaging/Email$$r #382

Closed novumcoder closed 2 years ago

novumcoder commented 2 years ago

Hi, im using the fflib_SObjectUnitOfWork apex class to handle the DML operations within my Queueable class.

The queueable class is constructed properly but once i enqueue it via System.enqueueJob it throws an error on the log:

System.SerializationException: Not Serializable: com/salesforce/api/fast/List$$lcom/salesforce/api/Messaging/Email$$r

It does not give me any line no on any apex class. Why is this failing? Where is this serialization happening?

I put a System.debug call in my "public void execute(QueueableContext context)" method, but this debug log is not coming up, so even before the queueable is executed this error happens, but the constructor is fine. Whats wrong?

Is the fflib_SObjectUnitOfWork working fine with Queueable?

daveespo commented 2 years ago

Is the fflib_SOUOW an instance variable for your Queuable?

If so, I suspect that's the problem. The instance state of the Queuable needs to be serializable and it sounds like one of the objects you've pushed into the SOUOW isn't serializable. Based on the error, it sounds like you probably called registerEmail on the SOUOW and there's a SingleEmailMessage sitting in the SOUOW.

tfuda commented 2 years ago

I'd be curious... do you have multiple currencies enabled within this org? Is the CurrencyISOCode field present on your SObject tables? I experienced this error (Not Serializable, with the error coming from some Salesforce internal API) in one of my Visualforce pages just recently and could not pin it down to a line number either. The problem only occurred once I enabled multiple currencies. I didn't dig into it any further once I realized it was somehow tied to enabling multi-currency, because multi-currency had been enabled accidentally. I just switched to a different org.

novumcoder commented 2 years ago

Is the fflib_SOUOW an instance variable for your Queuable?

If so, I suspect that's the problem. The instance state of the Queuable needs to be serializable and it sounds like one of the objects you've pushed into the SOUOW isn't serializable. Based on the error, it sounds like you probably called registerEmail on the SOUOW and there's a SingleEmailMessage sitting in the SOUOW.

Hi, i have a basic Dev Org registered with no special settings implementing this Queueable and the fflib stuff. I never used "registerEmail". I analyzed the code of fflib_SOUOW and have seen the email stuff is always running. I was only dealing with 2 custom objects to be added as New or Dirty. But still the fflib code has hardcoded lines of adding a "SingleEmailMessage" in its internal list. So this is why its failing. But that definitely means fflib_SOUOW is NOT supported within Queueables, right? Check the code for yourself.

stohn777 commented 2 years ago

Hi @novumcoder.

The email functionality is indeed instantiated and executes by default, however the SendEmailWork.emails list is empty by default, which may be sufficient for synchronous work. However Queueable is asynchronous which leads me to question sending a UoW into the Queueable class. My first thought about such a design is that each instance of the Queueable class would instance its UoW and commit-work during is contextual life cycle.

With that said and if you have a viable use case for sending the UoW into a Queueable class, I'd enjoy seeing your suggestion for enhancing the UoW class to defer the IEmailWork functionality until actually needed.

Thanks.

novumcoder commented 2 years ago

Hi @stohn777 ,

the only way for me to make it work is to entirely remove any reference to SingleEmailMessage and Email (base class). I analyzed the code and it is always (!) instantiating the email stuff.

Best way is to have a clone of the UoW class without any email features. Because once you have a reference to Email, the queueable will fail once scheduled. Wasn't able to get it working without removing all the references, i probably broke other parts but at least my queueable was working.

stohn777 commented 2 years ago

@novumcoder

Thanks for the prompt reply.

Due to the nature of Salesforce's execution of Queueable work, I re-emphasize my question about the use case for passing serialized UoW instances into it.

stohn777 commented 2 years ago

Hi @novumcoder

While playing with Finalizers a bit this weekend and with them being Queueable, I decided to test this Issue regarding the UoW. In both a default scratch org and one with multi-currency enabled, per @tfuda curiosity, the "FinalizationWithFflibTest" class completed without issue. Below is the test code that I used.

Hopefully this might help you track-down the issue that you're facing.

attn: @daveespo @ImJohnMDaniel

Execute Anonymous Script:

// Prerequisite:
// Create an Account in the org and
// replace the Account.Id below

Id accountId = 'place_your_account_Id_here';

Account a = 
[
    select 
        id, name, description 
    from account 
    where id = :accountId
];
a.Description = 'Hello';
update a;

System.debug(a);

FinalizationWithFflibTest f = new FinalizationWithFflibTest();
f.setAccount(a);
System.enqueueJob(f);

Apex Class

public class FinalizationWithFflibTest
    implements Finalizer, Queueable 
{
    private Account acct = null;

    public Account getAccount()
    {
        return acct;
    }

    public Account setAccount(Account value)
    {
        acct = value;
        return acct;
    }

    private FinalizationWithFflibTest attachToFinalizer()
    {
        FinalizationWithFflibTest f = new FinalizationWithFflibTest();
        System.attachFinalizer(f);
        return f;
    }

    private Account selectAccountById(Id accountId)
    {
        System.assert(accountId != null);

        String soql = 'select Id, Name, Description from Account where Id = :accountId';
        return (Account) Database.query(soql);
    }

    private fflib_SObjectUnitOfWork getUoW()
    {
        return
            new fflib_SObjectUnitOfWork(new List<Schema.SObjectType> {Account.SObjectType});
    }

    private Account supplementAccountDescription(Account a, String value)
    {
        a = selectAccountById(acct.Id);

        if (String.isNotBlank(a.Description) && !value.contains('!')) a.Description += ' ';
        a.Description += value;

        fflib_SObjectUnitOfWork uow = getUoW();
        uow.registerDirty(a);
        uow.commitWork();

        return a;
    }

    public void execute(QueueableContext ctx)
    {
        FinalizationWithFflibTest f = attachToFinalizer();

        Account a = getAccount();
        a = supplementAccountDescription(a, 'world');

        f.setAccount(a);

        System.debug(ctx);
        System.debug(a);
    }

    public void execute(FinalizerContext ctx)
    {
        Account a = getAccount();
        a = supplementAccountDescription(a, '!!!');

        System.debug(ctx);
        System.debug(a);
    }
}
daveespo commented 2 years ago

@novumcoder -- I think there is a misunderstanding of my prior comment. I asked if your SOUOW is an instance variable on your Queueable class and you didn't answer the question.

This is supported:

public with sharing class SOUOWQueueable implements Queueable {
    public SOUOWQueueable(){
    }

    public void execute(QueueableContext qc){
        fflib_SObjectUnitOfWork souow = 
            new fflib_SObjectUnitOfWork(new List<Schema.SObjectType>{
                Account.SobjectType
            }
        );

        souow.registerNew(new Account(Name='Test'));

        souow.commitWork();
    }
}

This is not (results in the SerializationException reported in the description):

public with sharing class SOUOWQueueable implements Queueable {
    fflib_SObjectUnitOfWork souow;
    public SOUOWQueueable(){
        souow = new fflib_SObjectUnitOfWork(new List<Schema.SObjectType>{
                Account.SobjectType
            }
        );

        souow.registerNew(new Account(Name='Test'));

    }

    public void execute(QueueableContext qc){
        souow.commitWork();
    }
}
novumcoder commented 2 years ago

@novumcoder -- I think there is a misunderstanding of my prior comment. I asked if your SOUOW is an instance variable on your Queueable class and you didn't answer the question.

This is supported:

public with sharing class SOUOWQueueable implements Queueable {
  public SOUOWQueueable(){
  }

  public void execute(QueueableContext qc){
      fflib_SObjectUnitOfWork souow = 
          new fflib_SObjectUnitOfWork(new List<Schema.SObjectType>{
              Account.SobjectType
          }
      );

      souow.registerNew(new Account(Name='Test'));

      souow.commitWork();
  }
}

This is not (results in the SerializationException reported in the description):

public with sharing class SOUOWQueueable implements Queueable {
  fflib_SObjectUnitOfWork souow;
  public SOUOWQueueable(){
      souow = new fflib_SObjectUnitOfWork(new List<Schema.SObjectType>{
              Account.SobjectType
          }
      );

      souow.registerNew(new Account(Name='Test'));

  }

  public void execute(QueueableContext qc){
      souow.commitWork();
  }
}

Hi @daveespo,

i didn't know that. In fact the variable was an instance of the Queueable class. So it only works if its an instance within the execute method context only. I will try that. Thanks.

cropredyHelix commented 1 year ago

This was a good discussion; a quick tldr for others reading this...