0

In attempting to use the Org Platform Cache as a 'transaction-spanning' variable, we tested the following scenario involving concurrent transactions:

Transaction 1:

//assume the Cache.Org.someBool value is false

Cache.Org.put('someBool',true);
//5 second delay
Cache.Org.put('someBool',false);

Concurrent Transaction 2:

When we read the value of someBool from the Org Platform Cache during the 5 second delay from a different transaction, we expect to read true. However, the value is false, despite reading this during the delay before it is reverted back to false.

This led us to believe that maybe the platform cache isn't written to until the transaction has finished. So then we left the value as true and didn't revert to false at the end of the 5 second delay.

Reading the value of someBool during the 5 second delay in this case resulted in the value true.

Can anyone explain why the Platform Cache behaves this way?

4
  • 1
    That would tally with all other transaction behaviours in Salesforce and makes absolute sense to me. The data gets written to persistent store at the end of the transaction for all other types (DML, async enqueuement etc., except for "publish immediately" platform events). This allows for clean recovery in case the transaction fails (except for publish immediately that happens whether the parent transaction succeeds or fails).
    – Phil W
    Oct 21, 2023 at 15:31
  • I did think that also, so tested this by removing the second write and then retrieving the value while the first transaction was ongoing and it retrieved the newly set value even though the transaction hadn't completed
    – edralph
    Oct 21, 2023 at 15:34
  • 1
    The last part of your investigation is interesting, though I wonder if you should try this out with a string value rather than a Boolean (to avoid obvious pitfalls with default values etc.)
    – Phil W
    Oct 21, 2023 at 16:02
  • The cache implementation (based on Redis) is eventually rather than strongly consistent. This is by design and documented: At the same time, another transaction updates the same key with another value. Both writes succeed, but one of the two values is chosen arbitrarily as the winner, and later transactions read that one value. However, this arbitrary choice is per key rather than per transaction . Key versioning is one solution.
    – identigral
    Oct 22, 2023 at 17:57

1 Answer 1

1

The cache methods are not documented in regards to specific behavior, which is likely intentional. I ran some experiments on some data, and the results are kind of interesting.

I started off with some simple code:

@RemoteAction public static String delayCacheReadWrite(
    String newValue, 
    Integer readWriteDelay,
    Integer readDelay) {
  sleep(readWriteDelay);
  String result1 = readCache();
  writeCache(newValue);
  sleep(readDelay);
  String result2 = readCache();
  return result1+';'+result2;
}

Running this method directly with three values and certain timings within milliseconds of each other produces this output:

newValue readWriteDelay readDelay Output
Value 1 0 2000 Default;Value 1
Value 2 1500 500 Default;Value 2
Value 3 1000 1000 Default;Value 3

In other words, given a write at 0ms, 1000ms, and 2000ms, all three processes saw the default value on the first read, and their own set value on the second read, even though it's clear that if the cache were updated in close-to-real-time, the Value 1 input should have seen either Value 2 or possibly even Value 3. Each process only saw their own values.

However, things get more interesting for staggered runs. In this example, I added a 1250ms delay on the second input, and a 1750ms delay on the third input. The results are as follows:

newValue readWriteDelay readDelay setTimeout Output
Value 1 0 2000 0 Default;Value 1
Value 2 1500 500 1250 Value 1;Value 2
Value 3 1000 1000 1750 Value 1;Value 3

In this case, we can confirm that a write around 0ms is visible to the processes that started 1.25 seconds later and 1.75 seconds later, even though the first one hasn't finished yet.

A third set of data overlaps the calls closer together, and we might see:

newValue readWriteDelay readDelay setTimeout Output
Value 1 0 2000 0 Default;Value 1
Value 2 1500 500 500 Value 1;Value 2
Value 3 1000 1000 1000 Value 2;Value 3

Here, we start Value 2's read at 2,000, meaning the first transaction has committed, and Value 1 is available. Value 3 is set to start while Value 2 has 500ms left, approximately, and it sees Value 2, which was written at approximately 2000ms as well, even though Value 2's process has about 500ms to go.

However, running this experiment repeatedly shows this is finicky, and sometimes returns the outputs from the table immediately above, and sometimes the first table, and may even return some other combination not mentioned here. In other words, you should not attempt to use this as an inter-process communication, as it is unreliable at best.

Cache is used to store potentially expensive results so that at least some of the time, some transactions will run faster than they would otherwise.

If you want true inter-process communication, you could design something with a Platform Event marked as Deliver Immediately that creates a new record or updates and existing record, and then a FOR UPDATE query loop to query for the PE trigger-created records.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .