Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mv_jobflow.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Execute a series of job flows
4 @details Very (very) simple flow manager. Jobs execute in sequential waves,
5 all previous waves must finish successfully.
6
7 The input table is formed as per below. Each observation represents one job.
8 Each variable is converted into a macro variable with the same name.
9
10 ## Input table (minimum variables needed)
11
12 @li _PROGRAM - Provides the path to the job itself
13 @li FLOW_ID - Numeric value, provides sequential ordering capability. Is
14 optional, will default to 0 if not provided.
15 @li _CONTEXTNAME - Dictates which context should be used to run the job. If
16 blank, or not provided, will default to `SAS Job Execution compute context`.
17
18 Any additional variables provided in this table are converted into macro
19 variables and passed into the relevant job.
20
21 |_PROGRAM| FLOW_ID (optional)| _CONTEXTNAME (optional) |
22 |---|---|---|
23 |/Public/jobs/somejob1|0|SAS Job Execution compute context|
24 |/Public/jobs/somejob2|0|SAS Job Execution compute context|
25
26 ## Output table (minimum variables produced)
27
28 @li _PROGRAM - the SAS Drive path of the job
29 @li URI - the URI of the executed job
30 @li STATE - the completed state of the job
31 @li TIMESTAMP - the datetime that the job completed
32 @li JOBPARAMS - the parameters that were passed to the job
33 @li FLOW_ID - the id of the flow in which the job was executed
34
35 ![https://i.imgur.com/nZE9PvT.png](https://i.imgur.com/nZE9PvT.png)
36
37 To avoid hammering the box with many hits in rapid succession, a one
38 second pause is made between every request.
39
40
41 ## Example
42
43 First, compile the macros:
44
45 filename mc url
46 "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
47 %inc mc;
48
49 Next, create some jobs (in this case, as web services):
50
51 filename ft15f001 temp;
52 parmcards4;
53 %put this is job: &_program;
54 %put this was run in flow &flow_id;
55 data ;
56 rand=ranuni(0)*&macrovar1;
57 do x=1 to rand;
58 y=rand*&macrovar2;
59 if y=100 then abort;
60 output;
61 end;
62 run;
63 ;;;;
64 %mv_createwebservice(path=/Public/temp,name=demo1)
65 %mv_createwebservice(path=/Public/temp,name=demo2)
66
67 Prepare an input table with 60 executions:
68
69 data work.inputjobs;
70 _contextName='SAS Job Execution compute context';
71 do flow_id=1 to 3;
72 do i=1 to 20;
73 _program='/Public/temp/demo1';
74 macrovar1=10*i;
75 macrovar2=4*i;
76 output;
77 i+1;
78 _program='/Public/temp/demo2';
79 macrovar1=40*i;
80 macrovar2=44*i;
81 output;
82 end;
83 end;
84 run;
85
86 Trigger the flow
87
88 %mv_jobflow(inds=work.inputjobs
89 ,maxconcurrency=4
90 ,outds=work.results
91 ,outref=myjoblog
92 )
93
94 data _null_;
95 infile myjoblog;
96 input; put _infile_;
97 run;
98
99
100 @param [in] access_token_var= The global macro variable to contain the
101 access token
102 @param [in] grant_type= valid values:
103 @li password
104 @li authorization_code
105 @li detect - will check if access_token exists, if not will use
106 sas_services if a SASStudioV session else authorization_code. Default
107 option.
108 @li sas_services - will use oauth_bearer=sas_services
109 @param [in] inds= The input dataset containing a list of jobs and parameters
110 @param [in] maxconcurrency= The max number of parallel jobs to run. Default=8.
111 @param [in] raise_err=0 Set to 1 to raise SYSCC when a job does not complete
112 succcessfully
113 @param [in] mdebug= set to 1 to enable DEBUG messages
114 @param [out] outds= The output dataset containing the results
115 @param [out] outref= The output fileref to which to append the log file(s).
116
117 @version VIYA V.03.05
118 @author Allan Bowe, source: https://github.com/sasjs/core
119
120 <h4> SAS Macros </h4>
121 @li mf_nobs.sas
122 @li mp_abort.sas
123 @li mf_getplatform.sas
124 @li mf_getvarlist.sas
125 @li mf_existvarlist.sas
126 @li mv_jobwaitfor.sas
127 @li mv_jobexecute.sas
128
129 @sasjslint {"maxLineLength": 98}
130**/
131
132%macro mv_jobflow(inds=0,outds=work.mv_jobflow
133 ,maxconcurrency=8
134 ,access_token_var=ACCESS_TOKEN
135 ,grant_type=sas_services
136 ,outref=0
137 ,raise_err=0
138 ,mdebug=0
139 );
140%local dbg;
141%if &mdebug=1 %then %do;
142 %put &sysmacroname entry vars:;
143 %put _local_;
144 %put inds vars:;
145 data _null_;
146 set &inds;
147 putlog (_all_)(=);
148 run;
149%end;
150%else %let dbg=*;
151
152%local oauth_bearer;
153%if &grant_type=detect %then %do;
154 %if %symexist(&access_token_var) %then %let grant_type=authorization_code;
155 %else %let grant_type=sas_services;
156%end;
157%if &grant_type=sas_services %then %do;
158 %let oauth_bearer=oauth_bearer=sas_services;
159 %let &access_token_var=;
160%end;
161
162%mp_abort(iftrue=(&grant_type ne authorization_code and &grant_type ne password
163 and &grant_type ne sas_services
164 )
165 ,mac=&sysmacroname
166 ,msg=%str(Invalid value for grant_type: &grant_type)
167)
168
169%mp_abort(iftrue=("&inds"="0")
170 ,mac=&sysmacroname
171 ,msg=%str(Input dataset was not provided)
172)
173%mp_abort(iftrue=(%mf_existVarList(&inds,_PROGRAM)=0)
174 ,mac=&sysmacroname
175 ,msg=%str(The _PROGRAM column must exist on input dataset &inds)
176)
177%mp_abort(iftrue=(&maxconcurrency<1)
178 ,mac=&sysmacroname
179 ,msg=%str(The maxconcurrency variable should be a positive integer)
180)
181
182/* set defaults if not provided */
183%if %mf_existVarList(&inds,_CONTEXTNAME FLOW_ID)=0 %then %do;
184 data &inds;
185 %if %mf_existvarList(&inds,_CONTEXTNAME)=0 %then %do;
186 length _CONTEXTNAME $128;
187 retain _CONTEXTNAME "SAS Job Execution compute context";
188 %end;
189 %if %mf_existvarList(&inds,FLOW_ID)=0 %then %do;
190 retain FLOW_ID 0;
191 %end;
192 /* https://github.com/sasjs/adapter/pull/845#issuecomment-2956589644 */
193 retain _omitSessionResults "false";
194 set &inds;
195 &dbg. putlog (_all_)(=);
196 run;
197%end;
198
199%local missings;
200proc sql noprint;
201select count(*) into: missings
202 from &inds
203 where flow_id is null or _program is null;
204%mp_abort(iftrue=(&missings>0)
205 ,mac=&sysmacroname
206 ,msg=%str(input dataset has &missings missing values for FLOW_ID or _PROGRAM)
207)
208
209%if %mf_nobs(&inds)=0 %then %do;
210 %put No observations in &inds! Leaving macro &sysmacroname;
211 %return;
212%end;
213
214/* ensure output table is available */
215data &outds;run;
216proc sql;
217drop table &outds;
218
219options noquotelenmax;
220%local base_uri; /* location of rest apis */
221%let base_uri=%mf_getplatform(VIYARESTAPI);
222
223
224/* get flows */
225proc sort data=&inds;
226 by flow_id;
227run;
228data _null_;
229 set &inds (keep=flow_id) end=last;
230 by flow_id;
231 if last.flow_id then do;
232 cnt+1;
233 call symputx(cats('flow',cnt),flow_id,'l');
234 end;
235 if last then call symputx('flowcnt',cnt,'l');
236run;
237
238/* prepare temporary datasets */
239%local fid jid jds jdsapp jdsrunning jdswaitfor;
240data;run;%let jds=&syslast;
241data;run;%let jdsapp=&syslast;
242data;run;%let jdsrunning=&syslast;
243data;run;%let jdswaitfor=&syslast;
244
245/* start loop */
246%do fid=1 %to &flowcnt;
247
248 %if not ( &raise_err and &syscc ) %then %do;
249
250 %put preparing job attributes for flow &&flow&fid;
251 %local jds jcnt;
252 data &jds(drop=_contextName _program);
253 set &inds(where=(flow_id=&&flow&fid));
254 if _contextName='' then _contextName="SAS Job Execution compute context";
255 call symputx(cats('job',_n_),_program,'l');
256 call symputx(cats('context',_n_),_contextName,'l');
257 call symputx('jcnt',_n_,'l');
258 &dbg. if _n_= 1 then putlog "Loop &fid";
259 &dbg. putlog (_all_)(=);
260 run;
261 /* build list of char and num vars in json format */
262 /* Viya 2026 expects all values to be strings */
263 %local nvars cvars ii _vnm;
264 %let cvars=%mf_getvarlist(&jds,typefilter=C);
265 %let nvars=%mf_getvarlist(&jds,typefilter=N);
266 %put exporting job variables in json format;
267 %do jid=1 %to &jcnt;
268 data _null_;
269 set &jds;
270 if _n_=&jid;
271 length _param $32767;
272 _param='';
273 %if %length(&cvars)>0 %then %do ii=1 %to %sysfunc(countw(&cvars,%str( )));
274 %let _vnm=%scan(&cvars,&ii,%str( ));
275 if _param ne '' then _param=cats(_param,',');
276 /* The keys are V7 SAS names (from mf_getvarlist) so need no
277 * escaping. The values are free-form data - eg SASJS-formatted
278 * CSV payloads with embedded doubled quotes and CRLF row
279 * separators - so must be json escaped. The sas quote() function
280 * doubles any embedded double quotes (csv convention) which is
281 * invalid json and makes the jobExecution request fail with a
282 * 400 response. Raw control characters (CR/LF) are also invalid
283 * json - viya silently replaces them with spaces, corrupting
284 * multi-line CSV payloads. Escape backslashes first, then CR,
285 * LF, and finally double quotes. */
286 _param=cats(_param,'"'
287 ,"&_vnm"
288 ,'":"'
289 ,tranwrd(tranwrd(tranwrd(tranwrd(trim(&_vnm),'\','\\'),'0D'x,'\r'),'0A'x,'\n'),'"','\"')
290 ,'"'
291 );
292 %end;
293 %if %length(&nvars)>0 %then %do ii=1 %to %sysfunc(countw(&nvars,%str( )));
294 %let _vnm=%scan(&nvars,&ii,%str( ));
295 if _param ne '' then _param=cats(_param,',');
296 _param=cats(_param,'"'
297 ,"&_vnm"
298 ,'":"'
299 ,strip(put(&_vnm,best32.))
300 ,'"'
301 );
302 %end;
303 call symputx(cats('jparams',&jid),_param,'l');
304 stop;
305 run;
306 %local jobuid&jid;
307 %let jobuid&jid=0; /* used in next loop */
308 %end;
309 %local concurrency completed;
310 %let concurrency=0;
311 %let completed=0;
312 proc sql; drop table &jdsrunning;
313 %do jid=1 %to &jcnt;
314 /**
315 * now we can execute the jobs up to the maxconcurrency setting
316 */
317 %if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
318
319 /* check to see if the job finished in the previous round */
320 %if %sysfunc(exist(&outds))=1 %then %do;
321 %local jobcheck; %let jobcheck=0;
322 proc sql noprint;
323 select count(*) into: jobcheck
324 from &outds where uuid="&&jobuid&jid";
325 %if &jobcheck>0 %then %do;
326 %put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
327 %let job&jid=0;
328 %end;
329 %end;
330
331 /* check if job was triggered and, if
332 so, if we have enough slots to run? */
333 %if ("&&jobuid&jid"="0") and (&concurrency<&maxconcurrency) %then %do;
334
335 /* But only start if no issues detected so far */
336 %if not ( &raise_err and &syscc ) %then %do;
337
338 %local jobname jobpath;
339 %let jobname=%scan(&&job&jid,-1,/);
340 %let jobpath=
341 %substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
342
343 /* the paramstring holds free-form data - an escaped double quote
344 * followed by a single quote reads as an unterminated literal and
345 * stops the macro, so mask it */
346 %put executing &jobpath/&jobname with paramstring %superq(jparams&jid);
347 %mv_jobexecute(path=&jobpath
348 ,name=&jobname
349 ,paramstring=%superq(jparams&jid)
350 ,outds=&jdsapp
351 ,contextname=&&context&jid
352 ,mdebug=&mdebug
353 )
354 data &jdsapp;
355 format jobparams $32767.;
356 set &jdsapp(where=(method='GET' and rel='state'));
357 jobparams=symget("jparams&jid");
358 /* uri here has the /state suffix */
359 uuid=scan(uri,-2,'/');
360 call symputx("jobuid&jid",uuid,'l');
361 run;
362 proc append base=&jdsrunning data=&jdsapp;
363 run;
364 %let concurrency=%eval(&concurrency+1);
365 /* sleep one second after every request to smooth the impact */
366 data _null_;
367 call sleep(1,1);
368 run;
369
370 %end;
371 %else %do; /* Job was skipped due to problems */
372
373 %put jobid &&job&jid in flow &fid skipped due to SYSCC (&syscc);
374 %let completed = %eval(&completed+1);
375 %let job&jid=0; /* Indicate job has finished */
376
377 %end;
378
379 %end;
380 %end;
381 %if &jid=&jcnt %then %do;
382 /* we are at the end of the loop - check which jobs have finished */
383 %mv_jobwaitfor(ANY,inds=&jdsrunning,outds=&jdswaitfor,outref=&outref
384 ,raise_err=&raise_err,mdebug=&mdebug)
385 %local done;
386 %let done=%mf_nobs(&jdswaitfor);
387 %if &done>0 %then %do;
388 %let completed=%eval(&completed+&done);
389 %let concurrency=%eval(&concurrency-&done);
390 data &jdsapp;
391 set &jdswaitfor;
392 flow_id=&&flow&fid;
393 uuid=scan(uri,-1,'/');
394 run;
395 proc append base=&outds data=&jdsapp;
396 run;
397 %end;
398 proc sql;
399 delete from &jdsrunning
400 where uuid in (select uuid from &outds
401 where state in ('canceled','completed','failed')
402 );
403
404 /* loop again if jobs are left */
405 %if &completed < &jcnt %then %do;
406 %let jid=0;
407 %put looping flow &fid again;
408 %put &completed of &jcnt jobs completed, &concurrency jobs running;
409 %end;
410 %end;
411 %end;
412
413 %end;
414 %else %do;
415
416 %put Flow &&flow&fid skipped due to SYSCC (&syscc);
417
418 %end;
419 /* back up and execute the next flow */
420%end;
421
422%if &mdebug=1 %then %do;
423 %put &sysmacroname exit vars:;
424 %put _local_;
425%end;
426
427%mend mv_jobflow;