Any suggestions for dealing with governance in a m...
# suitescript
m
Any suggestions for dealing with governance in a map/reduce? I'm getting a lot of
SSS_USAGE_LIMIT_EXCEEDED
which I believe is due to the various actions performed in a reduce context.
The reduce loops through 1 journal record for all journal lines with a certain account, does a search to find a purchase order based on a custom field value on the journal line, and then sets the resulting value on the journal line. Doesn't seem like it should be too much governance to me? Though it can run hundreds of times for a single journal entry (meaning there might be hundreds of lines with the relevant expense account on that journal)
reduce code
Assuming I'm right and the 100s of searches is what's hitting the SSS_USAGE_Limit (seems to coincide with the error stack) the only thing I can think of is to loop through the journal twice. Once to get all the po ids, then do a single search to build a big hash map, and then a second loop through the lines to check against the hash map?
The reduce runs... something like 90 times (once per relevant journal entry) though this will increase by 1 every month I think
b
do one search for all your purchase orders instead of one per line
your implementation will require you to use a bunch of "OR"s in your filter expression, though you can make the search easier for yourself if you can get actual internal ids of purchase orders instead of po number
m
Yeah unfortunately the only data on the journal line is the tranid
I'd make a big array and do anyof i suppose
pull all the records with internalid and tranid columns into a big array and then loop through the entries a second time and check the array
b
anyof is for select fields, tranid is not a select
m
looks like there's hopefully an Any option though
Hmm that didnt' save right, I guess Any is just the default dropdown option?
Maybe a query instead of a search at that point? I assume there'll be some governance issue with 1000 tranids
b
no real difference between a query or search here
you have the same options
a bunch of ors, or a in condition
with the in condition being limited to 1000 ids before you need to start making multiple of them for the ors
m
Okay, I just assumed the search would have tighter governance there
b
again, usually you just start with a bunch of 'or's
m
Unfortunately I don't know the maximum number of tranids i need to plan for here
b
usual limit is 10000, so either query or search isnt going to run out of points if you write is correctly
m
okay thanks battk i'll start working on figuring this out
a
My humble advice is to understand how Map Reduces works, once you fully understand them you can start to properly and efficiently design/architect your solution: If I'm not mistaken NetSuite Map Reduces are an implementation or fork of this: https://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf If you wan to skip that, then this: https://docs.oracle.com/en/cloud/saas/netsuite/ns-online-help/section_4387799161.html#bridgehead_1518486832
A properly designed Map Reduce will never run out of governance.
💯 1
l
@mrob to get all internal ids from tranid in one search you can user a formula like this: CASE WHEN {tranid} IN (‘PO1234’, ‘PO4567’) then 1 else 0 end EQUAL TO 1 I usually use Array.join(‘,’) to create the group
also, you could make your input data return only the lines that needs to be updated, on map you find the PO, then on reduce you update all lines together
w
Copy code
SELECT *
FROM transactionLines TL
   LEFT JOIN transaction PO_T on PO_T.[documentnumber] = TL.[memo/where thereference is]
WHERE TL.transaction = ? --use your starting journal as a starting point.
AND PO_T.id is not null
I still feel like this could be resolved with a query in getInputData that only returns the lines that you need to work on.
☝🏻 1
Pass all transaction lines to Map. Group them by transaction Id as key and keep all values as values in Reduce, process the lines that are included in values. Preferrably you will include lineuniquekey or lineid from getInputData. You will also retrieve all relevant values from the PO in getInputData as well. So that when you actually update the journal, it will only require the governance units needed for load and save on the journal (perhaps some other small stuff)
m
Thanks Watz and Luiz! I'll dig into those comments
Re: Only passing journal lines to map/reduce Currently the getinputdata does 2 things- 1. finds journals with lines with expense account 20003 2. finds inactive vendors associated with those journals and temporarily re-activates them (writes internal ids to a file in the file cabinet then read by the summarize) Then the reduce- 1. Loops through journal, identifying only relevant lines, searches for an associated PO for those relevant lines, then sets the value on the line Summary- 1. re-active vendors inactivated in getinputdata (read from the file in the file cabinet) So I don't understand how finding the lines themselves in getinputdata makes a difference in regards to governance if I kept this same structure? Meaning, in my perspective the thing I need to do is not change what's being passed to the reduce method, but instead simply figure out a way to only perform a single search after having looped through all the journal lines. I do understand how I could potentially find this information in the getinputdata function, but it seems 'cleaner' or better segmented to me to do it in the reduce still (and either loop through the journal twice or use a query b/w the journal and the pos)?
I already have this suiteql for identifying inactive vendors-
Copy code
var suiteQL = `SELECT V.ID
               FROM Transaction T
               JOIN TransactionLine TL ON T.ID = TL.TRANSACTION
               JOIN Transaction PO ON PO.TRANID = TL.CUSTCOL_AD_PO_NUM_JOURNALS
               JOIN Vendor V ON V.ID = PO.ENTITY
               WHERE T.TYPE = 'Journal' AND V.ISINACTIVE = 'T' AND TL.EXPENSEACCOUNT IN ('${expenseAccountId}')`;
so I assume I can just cut it short and do something like...
Copy code
SELECT PO.TRANID
FROM Transaction T
JOIN TransactionLine TL on T.ID = TL.TRANSACTION
JOIN Transaction PO on PO.TRANID = TL.CUSTCOL_AD_PO_NUM_JOURNALS 
WHERE T.TYPE = 'Journal' AND PO.TYPE = 'Purchase Order' AND T.ID is ('${journal.id}')
l
I’d split this processing in : 1. Get Input Data: return your query to that so each line will be one execution of map 2. Map: Make Vendors active and map PO Id to object with Line Number, PO Internal Id and any other data needed on Reduce and write it with journal entry internal id 3. Reduce: All lines of same journal will on same execution of reduce, load JE, iterate all lines to update journal entry, save it then inactive all vendors again. 4. Summarize: just return execution errors.
m
Interesting so Map would run 1000s of times but since it's writing context with the same key the Reduce would only runs 10s of times? (this would be done by shuffle stage essentially)
For an idea on total performance, for now the script will be reviewing/updating ~50,000 journal lines, but every month that number will grow (ideally the number that actually are changed dramatically drops after first pass but all 50K+ need to be reviewed for changes every month)
w
You should minimize the "reviewing" by using suiteql to only get the lines that you can update.
In Luiz proposed division of work across the stages, watch out for two map-instances modifying the same vendor. Also, in reduce. Make sure that you don't have too many vendors to activate again. It need to fit inside the 5000 points.
m
Yeah I think I'd have to do the vendors in getinput/summary still
I do think just doing a query in the reduce statements so it runs once/journal will work but I do agree that journal lines in map and journals in reduce is a better structure
w
How many vendors could a single journal contain?
m
hundreds or thousands
They are sort-of a custom type thing built for a revenue recognition process
w
Doing it in getinput and summarize isn't really that much better as you're limited to 10000 points. For all journals.
m
Yeah, for the moment I believe i'm activating 350 vendors
which is a single submitfields for each vendor
So I guess it's 350 x 5 which you're right, is still a notable governance but I think still works in my 10K limit for the moment... I don't expect the inactive vendors to dramatically increase in the near future either. And I do need to get these 4 map/reduce done asap also so I think even if a re-factor for vendor activation is needed it can happen after these go live.
n
As long as you know the 350 records up front in theory you could chunk this and call a SuiteLet one or more times to process the submitfields part. A Suitelet has a governance of 1k, lets say you process 175 in each SuiteLet (175*5 = 875), so that's within the 1k limits and the cost to call the SuiteLet is 10 governance, that's a total of 20 governance in your example. ((350/175)*10). I'm saying this without fully considering the order you're doing things. You may need to create an array of promises and await the completion if you need to know they have all been updated before moving on.
w
300s / 175records = 1,7 seconds allowed per record.submitFields() You wouldn't need to use promises, right? But it might take 2*5 minutes for the two calls to complete instead of 5 minutes.
n
I don't know if you would need to, I'm just proposing that if you need to track the success/failure, and the SuiteLet was returning something meaningful, you may need to use an array of promises if you fired off 2 x 175 requests 😉
Of course time is also a factor not just the raw governance of the api calls. YMMV
w
But you can still track the success/failure if you're doing them synchronously. The only upside with promises would be parallel processing of suitelet-calls. Maybe I'm misunderstanding you. 🙂
n
Ah, sorry yes, I see what you mean, doing it asynchronously is not definitively relevant to tracking success / failure. 👍🏻 My thought was that if you need to know if all calls were successful it'd be more efficient with an array of promises as you can process the .success of each as they land and not wait. Anyhooooow this could all be moot, I just came back from a few days off, catching up with messages and thought it worth throwing it out as a potential option and may not even be relevant 🙂
🌴 1
m
Sorry for the delay but I believe I was successful with my query-
Copy code
var suiteQL = `SELECT PO.TRANID, PO.ENTITY
                      FROM Transaction T
                      JOIN TransactionLine TL ON T.ID = TL.TRANSACTION
                      JOIN Transaction PO ON PO.TRANID = TL.CUSTCOL_AD_PO_NUM_JOURNALS
                      WHERE T.TYPE = 'Journal'
                        AND PO.TYPE = 'PurchOrd'
                        AND T.ID IN ('${journalRecord.id}')
                        AND TL.EXPENSEACCOUNT IN ('${expenseAccountId}')
                        AND PO.ENTITY IS NOT NULL`;
for my vendor script (runs once per reduce/journal entry)
And
Copy code
var suiteQL = `SELECT PO.TRANID, PO.ENTITY AS PO_ENTITY, SO.ENTITY AS SO_ENTITY
        FROM Transaction J
        JOIN TransactionLine JL ON J.ID = JL.TRANSACTION
        JOIN Transaction PO ON PO.TRANID = JL.CUSTCOL_AD_PO_NUM_JOURNALS
        JOIN Transaction SO on SO.ID = PO.CUSTBODY_RSM_SO_REFERENCE
        WHERE J.TYPE = 'Journal'
          AND PO.TYPE = 'PurchOrd'
          AND SO.TYPE = 'SalesOrd'
          AND J.ID IN ('${journalRecord.id}')
          AND JL.EXPENSEACCOUNT IN ('${expenseAccountId}')
          AND PO.ENTITY IS NOT NULL`;
for my customer script which is running now fingers crossed