Technical Articles
Groovy Scripters, this was “def”initely a head-scratcher!!!
Hello Groovy Scripters,
I believe, Groovy has grown in popularity with the popularity of SAP Cloud Platform Integration Suite. Gartner naming SAP a Leader in 2020 Magic Quadrant for EiPaaS only adds to the popularity of SAP Cloud Platform Integration Suite, and by extension, that of Groovy. We have more content on Groovy Scripting in CPI than ever before with the release of Developing Groovy Scripts for SAP Cloud Platform Integration by Vadim Klimov and Eng Swee Yeoh along with countless blogs on Groovy on SAP Community and the wider internet.
Well, we better get well versed with all the best practices in Groovy Script.
Setting the Stage
Recently, I came across this script for mapping E1EDK01-ZTERM:
def String getTermsOfPaymentKey(String paymentTerms, String creditDays) {
// subtract the length of credit days from 3
creditDaysLength = 3 - (creditDays.length());
// substring paymentTerms
termsOfPaymentKey = paymentTerms.substring( 0, creditDaysLength + 1);
// concatenate creditDays to the substring of paymentTerms
termsOfPaymentKey = termsOfPaymentKey.concat(creditDays)
return termsOfPaymentKey;
}
The script is pretty simple.
Example calls are like:
getTermsOfPaymentKey('B007', '7');
getTermsOfPaymentKey('B007', '21');
The Script receives (default) paymentTerms (taken from customer master) in the form of B000, where 000 is a number of credit days and creditDays for a given order. The credit days for the order can be different from that in customer master.
Then, the script removes as many characters from paymentTerms as the length of creditDays in the variable termsOfPaymentKey.
Finally, the script concatenates creditDays to termsOfPaymentKey and returns termsOfPaymentKey.
So, for the two inputs above, the expected output is B007 and B021 respectively.
assert getTermsOfPaymentKey('B007', '7') == 'B007'
assert getTermsOfPaymentKey('B007', '21') == 'B021'
And, we get the output as expected when I run them in the GroovyIDE. Well then, the blog is over. Ummmm, not quite.
Ummmm, not quite
Corrodias disagree in their answer to What is the difference between defining variables using def and without? on StackOverflow.
You see, the script does the intended job except when it comes to performance. How so?
Have a closer look at these two lines:
// subtract the length of credit days from 3
creditDaysLength = 3 - (creditDays.length());
// substring paymentTerms
termsOfPaymentKey = paymentTerms.substring( 0, creditDaysLength + 1);
The catch here is that they don’t have def or type declaration. By definition, the variables creditDaysLength and termsOfPaymentKey are binding variables. Binding variables can be altered from outside the script object or created outside of a script and passed into it [5]. They are not supposed to be used in a multi-threaded context [5].
This means that when a lot of orders are sent to this mapping, it may produce results like:
B07
B0021
B007
B021
Because the script runs in separate threads at runtime, however, the variables are shared.
Let’s check the execution when the calls are made in separate threads.
Thread 1 is called with B007 and 7. Thread 2 has inputs B007 and 21.
Thread 1 (‘B007′,’7’) |
Thread 2 (‘B007′,’21’) | creditDaysLength | termsOfPaymentKey |
---|---|---|---|
creditDaysLength = 3 – (creditDays.length()); | 2 | ||
creditDaysLength = 3 – (creditDays.length()); | 1 | ||
termsOfPaymentKey = paymentTerms.substring( 0, creditDaysLength + 1); | B0 | ||
termsOfPaymentKey = termsOfPaymentKey.concat(creditDays) | B07 | ||
return termsOfPaymentKey; | B07 | ||
termsOfPaymentKey = paymentTerms.substring( 0, creditDaysLength + 1); | B0 | ||
termsOfPaymentKey = termsOfPaymentKey.concat(creditDays) | B021 | ||
return termsOfPaymentKey; | B021 |
In this run, it goes wrong when creditDaysLength is set to 1 by another thread. In the weirdest of runs, we might get B021 or B0021 for the input of B007 and 7. ?. In most cases, the thread gets the full attention of the CPU and the function returns the expected result. Sometimes, CPU gets to pull one over on us developers.
The Fix
The fix is simple. Unless you have a very specific reason to share the value of the variable after the script execution has finished, always use def or type declaration. When the script is written with the def declaration, it produces consistent and expected results even when under stress.
def String getTermsOfPaymentKey(String paymentTerms, String creditDays) {
// subtract the length of credit days from 3
def creditDaysLength = 3 - (creditDays.length());
// substring paymentTerms
def termsOfPaymentKey = paymentTerms.substring( 0, creditDaysLength + 1);
// concatenate creditDays to the substring of paymentTerms
termsOfPaymentKey = termsOfPaymentKey.concat(creditDays)
return termsOfPaymentKey;
}
Do you know a variable we commonly used that is a binding variable? It is usually found when logging the body.
import com.sap.gateway.ip.core.customdev.util.Message
def Message processData(Message message) {
def messageLog = messageLogFactory.getMessageLog(message)
messageLog.addAttachmentAsString('Body', message.getBody(String), null)
return message
}
The famous binding variable is messageLogFactory. It is defined at the parent class that runs the execution of every Groovy script in CPI and passes the object via a Binding [4].
For the curious one’s, we ended up dropping the script completely and used standard functions like so:
Terms of Payment Key Mapping
We take the first letter of Payment Terms, format the Credit Days using the format 000 and concatenate the two together to get the terms of payment key.
It was fun for me to figure this one out. Hopefully, you had just as much fun reading the blog. Not every mapping can be solved using standard mapping, so, knowing how Groovy works will definitely help you.
Hope this helps,
Bala
References
- Question: What is the difference between defining variables using def and without?
- Corrodias‘s answer to What is the difference between defining variables using def and without?
- Avoid Binding Variables in Groovy Scripts by Markus Muenkel
- Eng Swee Yeoh‘s suggestion
- Groovy Documentation on Binding class
The specific term for it is called a binding (or binding variable) rather than global variable. The following blog describes more about it.
https://blogs.sap.com/2017/06/22/avoid-binding-variables-in-groovy-scripts/
messageLogFactory is not really a global variable that is accessible by separate threads. If it were, then all the MPL attachments would be messed up as many threads are accessing it. Instead, it is defined at the parent class that runs the execution of every Groovy script in CPI, and passes the object via a Binding.
Thank you Eng Swee Yeoh.
Sanjali Salian told me this was documented by SAP before, however, I couldn’t find it.
I have added the reference and also updated the text. Please let me know if any further suggestions.
Thank you for sharing this post, Bhalchandra Wadekar!
It is something very easy to fix once you know the reason. But if you have no knowledge about this mechanism it can cost a lot of development time & lost hair. I know feel vaccinated for this specific bug.
Best regards,
Fatih
Nice blog. Great help and good practice to follow for all grookies 😉