mirror of
https://github.com/sasjs/core.git
synced 2025-12-11 14:34:35 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f805b006f | ||
|
|
c6b65366b7 | ||
|
|
51ddd9c1e5 | ||
|
|
20bf3b86af | ||
|
|
de67cd329b | ||
|
|
779e4942c7 | ||
|
|
a69a1ac7f0 | ||
|
|
2a644d6c2b | ||
|
|
843930c666 | ||
|
|
90d69af7ee | ||
|
|
b7bafb49f4 | ||
|
|
2fa9e48286 | ||
|
|
5cee93c7bd | ||
|
|
1a595c64c6 | ||
|
|
2c901831b7 | ||
|
|
28209950ab | ||
|
|
44069e9867 | ||
|
|
e26af5c09a | ||
|
|
4ee13c9389 | ||
|
|
15f903aa42 | ||
|
|
58a0cce39e | ||
|
|
9a5574ea0e | ||
|
|
e6146dcbcf | ||
|
|
583c7e0c83 | ||
|
|
223bdd5983 | ||
|
|
aef14543f0 | ||
|
|
c88764c1d8 | ||
|
|
2c952c8b01 | ||
|
|
de3610d1aa | ||
|
|
d35d597437 | ||
|
|
3e8deda008 |
582
all.sas
582
all.sas
@@ -18,24 +18,12 @@
|
||||
options noquotelenmax;
|
||||
/**
|
||||
@file
|
||||
@brief abort gracefully according to context
|
||||
@details Do not use directly! See bottom of explanation for details.
|
||||
@brief to be deprecated
|
||||
@details We will deprecate this macro in 2022
|
||||
|
||||
Configures an abort mechanism according to site specific policies or the
|
||||
particulars of an environment. For instance, can stream custom
|
||||
results back to the client in an STP Web App context, or completely stop
|
||||
in the case of a batch run.
|
||||
As you can see, it's not a macro function.
|
||||
|
||||
For the sharp eyed readers - this is no longer a macro function!! It became
|
||||
a macro procedure during a project and now it's kinda stuck that way until
|
||||
that project is updated (if it's ever updated). In the meantime we created
|
||||
`mp_abort` which is just a wrapper for this one, and so we recomend you use
|
||||
that for forwards compatibility reasons.
|
||||
|
||||
@param mac= to contain the name of the calling macro
|
||||
@param type= deprecated. Not used.
|
||||
@param msg= message to be returned
|
||||
@param iftrue= supply a condition under which the macro should be executed.
|
||||
Use mp_abort.sas instead.
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
@@ -1636,13 +1624,24 @@ Usage:
|
||||
@details Configures an abort mechanism according to site specific policies or
|
||||
the particulars of an environment. For instance, can stream custom
|
||||
results back to the client in an STP Web App context, or completely stop
|
||||
in the case of a batch run.
|
||||
in the case of a batch run. For STP sessions
|
||||
|
||||
The method used varies according to the context. Important points:
|
||||
|
||||
@li should not use endsas or abort cancel in 9.4m3 environments as this can
|
||||
cause hung multibridge sessions and result in a frozen STP server
|
||||
@li should not use endsas in viya 3.5 as this destroys the session and cannot
|
||||
fetch results (although both mv_getjoblog.sas and the @sasjs/adapter will
|
||||
recognise this and fetch the log of the parent session instead)
|
||||
@li STP environments must finish cleanly to avoid the log being sent to
|
||||
_webout. To assist with this, we also run stpsrvset('program error', 0)
|
||||
and set SYSCC=0. For 9.4m3 we take a unique approach - we open a macro
|
||||
but don't close it! This provides a graceful abort, EXCEPT when called
|
||||
called within a %include within a macro (and that macro contains additional
|
||||
logic). See mp_abort.test.nofix.sas for the example case.
|
||||
If you know of another way to gracefully abort a 9.4m3 STP session, we'd
|
||||
love to hear about it!
|
||||
|
||||
Using SAS Abort Cancel mechanisms can cause hung sessions in some Stored
|
||||
Process environments. This macro takes a unique approach - we set the SAS
|
||||
syscc to 0, run `stpsrvset('program error', 0)` (if SAS 9) and then - we open
|
||||
a macro but don't close it! This provides a graceful abort for SAS web
|
||||
services in all web enabled environments.
|
||||
|
||||
@param mac= to contain the name of the calling macro
|
||||
@param msg= message to be returned
|
||||
@@ -1656,6 +1655,8 @@ Usage:
|
||||
%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1)
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%global sysprocessmode sysprocessname;
|
||||
|
||||
%if not(%eval(%unquote(&iftrue))) %then %return;
|
||||
|
||||
%put NOTE: /// mp_abort macro executing //;
|
||||
@@ -1663,9 +1664,7 @@ Usage:
|
||||
%put NOTE - &msg;
|
||||
|
||||
/* Stored Process Server web app context */
|
||||
%if %symexist(_metaperson)
|
||||
or (%symexist(SYSPROCESSNAME) and "&SYSPROCESSNAME"="Compute Server" )
|
||||
%then %do;
|
||||
%if %symexist(_metaperson) or "&SYSPROCESSNAME "="Compute Server " %then %do;
|
||||
options obs=max replace nosyntaxcheck mprint;
|
||||
/* extract log errs / warns, if exist */
|
||||
%local logloc logline;
|
||||
@@ -1695,7 +1694,7 @@ Usage:
|
||||
input;
|
||||
i=1;
|
||||
stoploop=0;
|
||||
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
|
||||
if _n_ ge &logline-15 and stoploop=0 then do until (i>22);
|
||||
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
|
||||
input;
|
||||
i+1;
|
||||
@@ -1760,32 +1759,60 @@ Usage:
|
||||
if debug ge '"131"' then put '>>weboutEND<<';
|
||||
run;
|
||||
|
||||
%let syscc=0;
|
||||
%if %symexist(_metaport) %then %do;
|
||||
%put _all_;
|
||||
|
||||
%if "&sysprocessmode " = "SAS Stored Process Server " %then %do;
|
||||
data _null_;
|
||||
if symexist('sysprocessmode')
|
||||
then if symget("sysprocessmode")="SAS Stored Process Server"
|
||||
then rc=stpsrvset('program error', 0);
|
||||
putlog 'stpsrvset program error and syscc';
|
||||
rc=stpsrvset('program error', 0);
|
||||
call symputx("syscc",0,"g");
|
||||
run;
|
||||
%if "%substr(&sysvlong.xxxxxxxxx,1,9)" ne "9.04.01M3" %then %do;
|
||||
%put NOTE: Ending SAS session due to:;
|
||||
%put NOTE- &msg;
|
||||
endsas;
|
||||
%end;
|
||||
%end;
|
||||
%else %if "&sysprocessmode " = "SAS Compute Server " %then %do;
|
||||
/* endsas kills the session making it harder to fetch results */
|
||||
data _null_;
|
||||
syswarningtext=symget('syswarningtext');
|
||||
syserrortext=symget('syserrortext');
|
||||
abort_msg=symget('msg');
|
||||
syscc=symget('syscc');
|
||||
sysuserid=symget('sysuserid');
|
||||
iftrue=symget('iftrue');
|
||||
put (_all_)(/=);
|
||||
abort cancel nolist;
|
||||
run;
|
||||
%end;
|
||||
/**
|
||||
* endsas is reliable but kills some deployments.
|
||||
* Abort variants are ungraceful (non zero return code)
|
||||
* This approach lets SAS run silently until the end :-)
|
||||
*/
|
||||
%put _all_;
|
||||
filename skip temp;
|
||||
data _null_;
|
||||
file skip;
|
||||
put '%macro skip(); %macro skippy();';
|
||||
run;
|
||||
%inc skip;
|
||||
%else %if "%substr(&sysvlong.xxxxxxxxx,1,9)" = "9.04.01M3" %then %do;
|
||||
/**
|
||||
* endsas kills 9.4m3 deployments by orphaning multibridges.
|
||||
* Abort variants are ungraceful (non zero return code)
|
||||
* This approach lets SAS run silently until the end :-)
|
||||
* Caution - fails when called within a %include within a macro
|
||||
* See tests/mp_abort.test.1 for an example case.
|
||||
*/
|
||||
filename skip temp;
|
||||
data _null_;
|
||||
file skip;
|
||||
put '%macro skip();';
|
||||
comment '%mend skip; -> fix lint ';
|
||||
put '%macro skippy();';
|
||||
comment '%mend skippy; -> fix lint ';
|
||||
run;
|
||||
%inc skip;
|
||||
%end;
|
||||
%else %do;
|
||||
%abort cancel;
|
||||
%end;
|
||||
%end;
|
||||
%else %do;
|
||||
%put _all_;
|
||||
%abort cancel;
|
||||
%end;
|
||||
%mend;
|
||||
%mend mp_abort;
|
||||
|
||||
/** @endcond *//**
|
||||
@file
|
||||
@@ -2909,20 +2936,24 @@ run;
|
||||
%mend;/**
|
||||
@file
|
||||
@brief Drops tables / views (if they exist) without warnings in the log
|
||||
@details
|
||||
@details Useful for dropping tables when you're not sure they exist, or if
|
||||
you are not sure whether they are a dataset or view. Also efficient for
|
||||
dropping multiple tables / views.
|
||||
|
||||
Example usage:
|
||||
|
||||
proc sql;
|
||||
create table data1 as select * from sashelp.class;
|
||||
create view view2 as select * from sashelp.class;
|
||||
%mp_dropmembers(list=data1 view2)
|
||||
%mp_dropmembers(data1 view2, libref=WORK)
|
||||
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_isblank.sas
|
||||
|
||||
|
||||
@param list space separated list of datasets / views
|
||||
@param libref= can only drop from a single library at a time
|
||||
@param list space separated list of datasets / views, WITHOUT libref
|
||||
@param libref= (WORK) Note - you can only drop from a single library at a time
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
@@ -2943,7 +2974,7 @@ run;
|
||||
delete &list;
|
||||
delete &list /mtype=view;
|
||||
run;
|
||||
%mend;/**
|
||||
%mend mp_dropmembers;/**
|
||||
@file
|
||||
@brief Create a CARDS file from a SAS dataset.
|
||||
@details Uses dataset attributes to convert all data into datalines.
|
||||
@@ -3385,19 +3416,19 @@ run;
|
||||
@returns The &outds table containing any bad rows, plus a REASON_CD column.
|
||||
|
||||
@param [in] inds The table to be checked, with the format above
|
||||
@param [in] targetds= The target dataset against which to verify VARIABLE_NM
|
||||
@param [in] targetds= The target dataset against which to verify VARIABLE_NM.
|
||||
This must be available (ie, the library must be assigned).
|
||||
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
|
||||
@param [out] outds= The output table, which is a copy of the &inds. table
|
||||
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||
processes should check this table (and return code) before continuing.
|
||||
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||
processes should check this table (and return code) before continuing.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_abort.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_getvartype.sas
|
||||
@li mf_nobs.sas
|
||||
@li mp_filtergenerate.sas
|
||||
@li mp_filtervalidate.sas
|
||||
|
||||
@@ -3437,41 +3468,52 @@ run;
|
||||
* quotes, commas, periods and spaces.
|
||||
* Only numeric values should remain
|
||||
*/
|
||||
%local reason_cd;
|
||||
%local reason_cd nobs;
|
||||
%let nobs=0;
|
||||
data &outds;
|
||||
/*length GROUP_LOGIC SUBGROUP_LOGIC $3 SUBGROUP_ID 8 VARIABLE_NM $32
|
||||
OPERATOR_NM $10 RAW_VALUE $4000;*/
|
||||
set &inds;
|
||||
length reason_cd $32;
|
||||
length reason_cd $4032;
|
||||
|
||||
/* closed list checks */
|
||||
if GROUP_LOGIC not in ('AND','OR') then do;
|
||||
REASON_CD='GROUP_LOGIC should be either AND or OR';
|
||||
REASON_CD='GROUP_LOGIC should be AND/OR, not:'!!cats(GROUP_LOGIC);
|
||||
putlog REASON_CD= GROUP_LOGIC=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if SUBGROUP_LOGIC not in ('AND','OR') then do;
|
||||
REASON_CD='SUBGROUP_LOGIC should be either AND or OR';
|
||||
REASON_CD='SUBGROUP_LOGIC should be AND/OR, not:'!!cats(SUBGROUP_LOGIC);
|
||||
putlog REASON_CD= SUBGROUP_LOGIC=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if mod(SUBGROUP_ID,1) ne 0 then do;
|
||||
REASON_CD='SUBGROUP_ID should be integer';
|
||||
REASON_CD='SUBGROUP_ID should be integer, not '!!left(subgroup_id);
|
||||
putlog REASON_CD= SUBGROUP_ID=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if upcase(VARIABLE_NM) not in
|
||||
(%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
|
||||
then do;
|
||||
REASON_CD="VARIABLE_NM not in &targetds";
|
||||
REASON_CD="Variable "!!cats(variable_nm)!!" not in &targetds";
|
||||
putlog REASON_CD= VARIABLE_NM=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if OPERATOR_NM not in
|
||||
('=','>','<','<=','>=','BETWEEN','IN','NOT IN','NE','CONTAINS')
|
||||
then do;
|
||||
REASON_CD='Invalid OPERATOR_NM';
|
||||
REASON_CD='Invalid OPERATOR_NM: '!!left(OPERATOR_NM);
|
||||
putlog REASON_CD= OPERATOR_NM=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
|
||||
@@ -3481,8 +3523,10 @@ data &outds;
|
||||
if substr(raw_value,1,1) ne '('
|
||||
or substr(cats(reverse(raw_value)),1,1) ne ')'
|
||||
then do;
|
||||
REASON_CD='Missing brackets in RAW_VALUE';
|
||||
REASON_CD='Missing start/end bracket in RAW_VALUE';
|
||||
putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
|
||||
@@ -3503,25 +3547,27 @@ data &outds;
|
||||
/* output records that contain values other than digits and spaces */
|
||||
if notdigit(compress(raw_value3,' '))>0 then do;
|
||||
putlog raw_value3= $hex32.;
|
||||
REASON_CD='Invalid RAW_VALUE';
|
||||
REASON_CD=cats('Invalid RAW_VALUE:',raw_value);
|
||||
putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
|
||||
run;
|
||||
|
||||
|
||||
data _null_;
|
||||
set &outds;
|
||||
call symputx('REASON_CD',reason_cd,'l');
|
||||
stop;
|
||||
set &outds end=last;
|
||||
putlog (_all_)(=);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&abort=YES and %mf_nobs(&outds)>0),
|
||||
%mp_abort(iftrue=(&abort=YES and &nobs>0),
|
||||
mac=&sysmacroname,
|
||||
msg=%str(Filter issues in &inds, reason: &reason_cd, details in &outds)
|
||||
msg=%str(Data issue: %superq(reason_cd))
|
||||
)
|
||||
|
||||
%if %mf_nobs(&outds)>0 %then %do;
|
||||
%if &nobs>0 %then %do;
|
||||
%let syscc=1008;
|
||||
%return;
|
||||
%end;
|
||||
@@ -3737,7 +3783,8 @@ filename &fref1 clear;
|
||||
run;
|
||||
%mp_abort(
|
||||
mac=&sysmacroname,
|
||||
msg=%str(Filter issues in &inref: %quote(&reason_cd))
|
||||
msg=%str(Filter validation issues. ERR=%superq(SYSERRORTEXT)
|
||||
, WARN=%superq(SYSWARNINGTEXT) )
|
||||
)
|
||||
%end;
|
||||
%let syscc=1008;
|
||||
@@ -5029,7 +5076,7 @@ create table &outds (rename=(
|
||||
)/*/STORE SOURCE*/;
|
||||
%put output location=&jref;
|
||||
%if &action=OPEN %then %do;
|
||||
OPTIONS NOBOMFILE;
|
||||
options nobomfile;
|
||||
data _null_;file &jref encoding='utf-8';
|
||||
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
||||
run;
|
||||
@@ -5682,7 +5729,15 @@ run;
|
||||
|
||||
%mp_searchcols(libs=sashelp work, cols=name sex age)
|
||||
|
||||
@param libs=
|
||||
@param libs=(SASHELP) Space separated list of libraries to search for columns
|
||||
@param cols= Space separated list of column names to search for (not case
|
||||
sensitive)
|
||||
@param outds=(mp_searchcols) the table to create with the results. Will have
|
||||
one line per table match.
|
||||
@param match=(ANY) The match type. Valid values:
|
||||
@li ANY - The table contains at least one of the columns
|
||||
@li WILD - The table contains a column with a name that partially matches
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
**/
|
||||
@@ -5690,6 +5745,7 @@ run;
|
||||
%macro mp_searchcols(libs=sashelp
|
||||
,cols=
|
||||
,outds=mp_searchcols
|
||||
,match=ANY
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%put &sysmacroname process began at %sysfunc(datetime(),datetime19.);
|
||||
@@ -5711,8 +5767,10 @@ create table _data_ as
|
||||
%end;
|
||||
order by 1,2,3;
|
||||
|
||||
%local tempds;
|
||||
%let tempds=&syslast;
|
||||
data &outds;
|
||||
set &syslast;
|
||||
set &tempds;
|
||||
length cols matchcols $32767;
|
||||
cols=upcase(symget('cols'));
|
||||
colcount=countw(cols);
|
||||
@@ -5726,10 +5784,29 @@ data &outds;
|
||||
retain matchcols;
|
||||
matchcols='';
|
||||
end;
|
||||
%if &match=ANY %then %do;
|
||||
if findw(cols,name,,'spit') then do;
|
||||
sumcols+1;
|
||||
matchcols=cats(matchcols)!!' '!!cats(name);
|
||||
end;
|
||||
%end;
|
||||
%else %if &match=WILD %then %do;
|
||||
if _n_=1 then do;
|
||||
retain wcount;
|
||||
wcount=countw(cols);
|
||||
drop wcount;
|
||||
end;
|
||||
do i=1 to wcount;
|
||||
length curword $32;
|
||||
curword=scan(cols,i,' ');
|
||||
drop curword;
|
||||
if index(name,cats(curword)) then do;
|
||||
sumcols+1;
|
||||
matchcols=cats(matchcols)!!' '!!cats(curword);
|
||||
end;
|
||||
end;
|
||||
%end;
|
||||
|
||||
if last.memname then do;
|
||||
if sumcols>0 then output;
|
||||
if sumcols=colcount then putlog "Full Match: " libname memname;
|
||||
@@ -5739,10 +5816,11 @@ run;
|
||||
|
||||
proc sort; by descending sumcols memname libname; run;
|
||||
|
||||
proc sql;
|
||||
drop table &tempds;
|
||||
%put &sysmacroname process finished at %sysfunc(datetime(),datetime19.);
|
||||
|
||||
%mend;
|
||||
/**
|
||||
%mend mp_searchcols;/**
|
||||
@file
|
||||
@brief Searches all data in a library
|
||||
@details
|
||||
@@ -8859,7 +8937,7 @@ data _null_;
|
||||
put ')/*/STORE SOURCE*/; ';
|
||||
put '%put output location=&jref; ';
|
||||
put '%if &action=OPEN %then %do; ';
|
||||
put ' OPTIONS NOBOMFILE; ';
|
||||
put ' options nobomfile; ';
|
||||
put ' data _null_;file &jref encoding=''utf-8''; ';
|
||||
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
|
||||
put ' run; ';
|
||||
@@ -10306,7 +10384,7 @@ run;
|
||||
#### &prefix.added
|
||||
|name:$32.|metaID:$17.|SAStabName:$32.|
|
||||
|---|---|---|
|
||||
|||DATA1|
|
||||
| | |DATA1|
|
||||
|
||||
#### &prefix.deleted
|
||||
|name:$32.|metaID:$17.|SAStabName:$32.|
|
||||
@@ -10317,7 +10395,7 @@ run;
|
||||
|tabName:$32.|tabMetaID:$17.|SAStabName:$32.|metaName:$32.|metaID:$17.|sasname:$32.|metaType:$16.|change:$64.|
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|TABLE2|A5XLSNXI.BK0001HN|TABLE2|c|A5XLSNXI.BM000MA9|c|Column|Deleted|
|
||||
||||d||d|Column|Added|
|
||||
| | | |d| |d|Column|Added|
|
||||
|
||||
#### &prefix.meta
|
||||
|Label1:$28.|cValue1:$1.|nValue1:D12.3|
|
||||
@@ -10338,9 +10416,8 @@ run;
|
||||
such as dangling metadata, embedded passwords, security issues and more.
|
||||
|
||||
@param [in] libname= the metadata name of the library to be compared
|
||||
@param [out] outlib= The output library in which to store the output tables.
|
||||
Default=WORK.
|
||||
@param [out] prefix The prefix for the four tables created. Default=metadiff.
|
||||
@param [out] outlib=(work) The library in which to store the output tables.
|
||||
@param [out] prefix=(metadiff) The prefix for the four tables created.
|
||||
|
||||
@version 9.3
|
||||
@author Allan Bowe
|
||||
@@ -13474,7 +13551,7 @@ data _null_;
|
||||
put ')/*/STORE SOURCE*/; ';
|
||||
put '%put output location=&jref; ';
|
||||
put '%if &action=OPEN %then %do; ';
|
||||
put ' OPTIONS NOBOMFILE; ';
|
||||
put ' options nobomfile; ';
|
||||
put ' data _null_;file &jref encoding=''utf-8''; ';
|
||||
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
|
||||
put ' run; ';
|
||||
@@ -15062,21 +15139,19 @@ run;
|
||||
/**
|
||||
@file
|
||||
@brief Extract the log from a completed SAS Viya Job
|
||||
@details Extracts log from a Viya job and writes it out to a fileref
|
||||
@details Extracts log from a Viya job and writes it out to a fileref.
|
||||
|
||||
To query the job, you need the URI. Sample code for achieving this
|
||||
is provided below.
|
||||
|
||||
## Example
|
||||
|
||||
First, compile the macros:
|
||||
|
||||
%* First, compile the macros;
|
||||
filename mc url
|
||||
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
|
||||
Next, create a job (in this case, a web service):
|
||||
|
||||
%* Next, create a job (in this case, a web service);
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
data ;
|
||||
@@ -15092,28 +15167,40 @@ run;
|
||||
;;;;
|
||||
%mv_createwebservice(path=/Public/temp,name=demo)
|
||||
|
||||
Execute it:
|
||||
|
||||
%* Execute it;
|
||||
%mv_jobexecute(path=/Public/temp
|
||||
,name=demo
|
||||
,outds=work.info
|
||||
)
|
||||
|
||||
Wait for it to finish, and grab the uri:
|
||||
|
||||
data _null_;
|
||||
%* Wait for it to finish;
|
||||
data work.info;
|
||||
set work.info;
|
||||
if method='GET' and rel='self';
|
||||
where method='GET' and rel='state';
|
||||
run;
|
||||
%mv_jobwaitfor(ALL,inds=work.info,outds=work.jobstates)
|
||||
|
||||
%* and grab the uri;
|
||||
data _null_;
|
||||
set work.jobstates;
|
||||
call symputx('uri',uri);
|
||||
run;
|
||||
|
||||
Finally, fetch the log:
|
||||
|
||||
%* Finally, fetch the log;
|
||||
%mv_getjoblog(uri=&uri,outref=mylog)
|
||||
|
||||
This macro is used by the mv_jobwaitfor.sas macro, which is generally a more
|
||||
convenient way to wait for the job to finish before fetching the log.
|
||||
|
||||
If the remote session calls `endsas` then it is not possible to get the log
|
||||
from the provided uri, and so the log from the parent session is fetched
|
||||
instead. This happens for a 400 response, eg below:
|
||||
|
||||
ErrorResponse[version=2,status=400,err=5113,id=,message=The session
|
||||
requested is currently in a failed or stopped state.,detail=[path:
|
||||
/compute/sessions/LONGURI-ses0006/jobs/LONGURI/log/content, traceId: 63
|
||||
51aa617d01fd2b],remediation=Correct the errors in the session request,
|
||||
and create a new session.,targetUri=<null>,errors=[],links=[]]
|
||||
|
||||
@param [in] access_token_var= The global macro variable to contain the access
|
||||
token
|
||||
@@ -15125,7 +15212,7 @@ run;
|
||||
if a SASStudioV session else authorization_code. Default option.
|
||||
@li sas_services - will use oauth_bearer=sas_services.
|
||||
@param [in] uri= The uri of the running job for which to fetch the status,
|
||||
in the format `/jobExecution/jobs/$UUID/state` (unquoted).
|
||||
in the format `/jobExecution/jobs/$UUID` (unquoted).
|
||||
@param [out] outref= The output fileref to which to APPEND the log (is always
|
||||
appended).
|
||||
|
||||
@@ -15183,7 +15270,7 @@ data _null_;
|
||||
call symputx('errflg',1);
|
||||
call symputx('errmsg',
|
||||
"URI should be in format /jobExecution/jobs/$$$$UUID$$$$"
|
||||
!!" but is actually like: &uri",'l');
|
||||
!!" but is actually like:"!!uri,'l');
|
||||
end;
|
||||
run;
|
||||
|
||||
@@ -15216,6 +15303,10 @@ proc http method='GET' out=&fname1 &oauth_bearer
|
||||
%end;
|
||||
;
|
||||
run;
|
||||
%if &mdebug=1 %then %do;
|
||||
%put &sysmacroname: fetching log loc from &uri;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
|
||||
%do;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
@@ -15253,43 +15344,74 @@ run;
|
||||
data _null_;
|
||||
infile &fname2;
|
||||
input;
|
||||
uri=_infile_;
|
||||
uri=cats(_infile_);
|
||||
if length(uri)<12 then do;
|
||||
call symputx('errflg',1);
|
||||
call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l');
|
||||
end;
|
||||
if scan(uri,1) ne 'files' or scan(uri,2) ne 'files' then do;
|
||||
else if (scan(uri,1,'/') ne 'compute' or scan(uri,2,'/') ne 'sessions')
|
||||
and (scan(uri,1,'/') ne 'files' or scan(uri,2,'/') ne 'files')
|
||||
then do;
|
||||
call symputx('errflg',1);
|
||||
call symputx('errmsg',
|
||||
"URI should be in format /files/files/$$$$UUID$$$$"
|
||||
!!" but is actually like: &uri",'l');
|
||||
"URI should be in format /compute/sessions/$$$$UUID$$$$/jobs/$$$$UUID$$$$"
|
||||
!!" or /files/files/$$$$UUID$$$$"
|
||||
!!" but is actually like:"!!uri,'l');
|
||||
end;
|
||||
else do;
|
||||
call symputx('errflg',0,'l');
|
||||
call symputx('logloc',uri,'l');
|
||||
end;
|
||||
call symputx('errflg',0,'l');
|
||||
call symputx('logloc',uri,'l');
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&errflg=1)
|
||||
%mp_abort(iftrue=(%str(&errflg)=1)
|
||||
,mac=&sysmacroname
|
||||
,msg=%str(&errmsg)
|
||||
)
|
||||
|
||||
/* we have a log uri - now fetch the log */
|
||||
%&dbg.put &sysmacroname: querying &base_uri&logloc/content;
|
||||
proc http method='GET' out=&fname1 &oauth_bearer
|
||||
url="&base_uri&logloc/content";
|
||||
url="&base_uri&logloc/content?limit=10000";
|
||||
headers
|
||||
%if &grant_type=authorization_code %then %do;
|
||||
"Authorization"="Bearer &&&access_token_var"
|
||||
%end;
|
||||
;
|
||||
run;
|
||||
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
|
||||
%do;
|
||||
|
||||
%if &mdebug=1 %then %do;
|
||||
%put &sysmacroname: fetching log content from &base_uri&logloc/content;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
|
||||
%if &SYS_PROCHTTP_STATUS_CODE=400 %then %do;
|
||||
/* fetch log from parent session */
|
||||
%let logloc=%substr(&logloc,1,%index(&logloc,%str(/jobs/))-1);
|
||||
%&dbg.put &sysmacroname: Now querying &base_uri&logloc/log/content;
|
||||
proc http method='GET' out=&fname1 &oauth_bearer
|
||||
url="&base_uri&logloc/log/content?limit=10000";
|
||||
headers
|
||||
%if &grant_type=authorization_code %then %do;
|
||||
"Authorization"="Bearer &&&access_token_var"
|
||||
%end;
|
||||
;
|
||||
run;
|
||||
%if &mdebug=1 %then %do;
|
||||
%put &sysmacroname: fetching log content from &base_uri&logloc/log/content;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
%end;
|
||||
|
||||
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201
|
||||
%then %do;
|
||||
%if &mdebug ne 1 %then %do; /* have already output above */
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
%mp_abort(mac=&sysmacroname
|
||||
,msg=%str(logfetch: &SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
|
||||
)
|
||||
%end;
|
||||
|
||||
data _null_;
|
||||
file "&fpath3..lua";
|
||||
put '
|
||||
@@ -16259,8 +16381,8 @@ run;
|
||||
run;
|
||||
|
||||
|
||||
@param [in] access_token_var= The global macro variable to contain the access
|
||||
token
|
||||
@param [in] access_token_var= The global macro variable to contain the
|
||||
access token
|
||||
@param [in] grant_type= valid values:
|
||||
@li password
|
||||
@li authorization_code
|
||||
@@ -16399,123 +16521,143 @@ data;run;%let jdswaitfor=&syslast;
|
||||
|
||||
/* start loop */
|
||||
%do fid=1 %to &flowcnt;
|
||||
%put preparing job attributes for flow &&flow&fid;
|
||||
%local jds jcnt;
|
||||
data &jds(drop=_contextName _program);
|
||||
set &inds(where=(flow_id=&&flow&fid));
|
||||
if _contextName='' then _contextName="SAS Job Execution compute context";
|
||||
call symputx(cats('job',_n_),_program,'l');
|
||||
call symputx(cats('context',_n_),_contextName,'l');
|
||||
call symputx('jcnt',_n_,'l');
|
||||
run;
|
||||
%put exporting job variables in json format;
|
||||
%do jid=1 %to &jcnt;
|
||||
data &jjson;
|
||||
set &jds;
|
||||
if _n_=&jid then do;
|
||||
output;
|
||||
stop;
|
||||
end;
|
||||
run;
|
||||
proc json out=&jfref;
|
||||
export &jjson / nosastags fmtnumeric;
|
||||
run;
|
||||
data _null_;
|
||||
infile &jfref lrecl=32767;
|
||||
input;
|
||||
jparams='jparams'!!left(symget('jid'));
|
||||
call symputx(jparams,substr(_infile_,3,length(_infile_)-4));
|
||||
run;
|
||||
%local jobuid&jid;
|
||||
%let jobuid&jid=0; /* used in next loop */
|
||||
%end;
|
||||
%local concurrency completed;
|
||||
%let concurrency=0;
|
||||
%let completed=0;
|
||||
proc sql; drop table &jdsrunning;
|
||||
%do jid=1 %to &jcnt;
|
||||
/**
|
||||
* now we can execute the jobs up to the maxconcurrency setting
|
||||
*/
|
||||
%if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
|
||||
|
||||
/* check to see if the job finished in the previous round */
|
||||
%if %sysfunc(exist(&outds))=1 %then %do;
|
||||
%local jobcheck; %let jobcheck=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: jobcheck
|
||||
from &outds where uuid="&&jobuid&jid";
|
||||
%if &jobcheck>0 %then %do;
|
||||
%put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
|
||||
%let job&jid=0;
|
||||
%if not ( &raise_err and &syscc ) %then %do;
|
||||
|
||||
%put preparing job attributes for flow &&flow&fid;
|
||||
%local jds jcnt;
|
||||
data &jds(drop=_contextName _program);
|
||||
set &inds(where=(flow_id=&&flow&fid));
|
||||
if _contextName='' then _contextName="SAS Job Execution compute context";
|
||||
call symputx(cats('job',_n_),_program,'l');
|
||||
call symputx(cats('context',_n_),_contextName,'l');
|
||||
call symputx('jcnt',_n_,'l');
|
||||
run;
|
||||
%put exporting job variables in json format;
|
||||
%do jid=1 %to &jcnt;
|
||||
data &jjson;
|
||||
set &jds;
|
||||
if _n_=&jid then do;
|
||||
output;
|
||||
stop;
|
||||
end;
|
||||
run;
|
||||
proc json out=&jfref;
|
||||
export &jjson / nosastags fmtnumeric;
|
||||
run;
|
||||
data _null_;
|
||||
infile &jfref lrecl=32767;
|
||||
input;
|
||||
jparams='jparams'!!left(symget('jid'));
|
||||
call symputx(jparams,substr(_infile_,3,length(_infile_)-4));
|
||||
run;
|
||||
%local jobuid&jid;
|
||||
%let jobuid&jid=0; /* used in next loop */
|
||||
%end;
|
||||
%local concurrency completed;
|
||||
%let concurrency=0;
|
||||
%let completed=0;
|
||||
proc sql; drop table &jdsrunning;
|
||||
%do jid=1 %to &jcnt;
|
||||
/**
|
||||
* now we can execute the jobs up to the maxconcurrency setting
|
||||
*/
|
||||
%if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
|
||||
|
||||
/* check to see if the job finished in the previous round */
|
||||
%if %sysfunc(exist(&outds))=1 %then %do;
|
||||
%local jobcheck; %let jobcheck=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: jobcheck
|
||||
from &outds where uuid="&&jobuid&jid";
|
||||
%if &jobcheck>0 %then %do;
|
||||
%put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
|
||||
%let job&jid=0;
|
||||
%end;
|
||||
%end;
|
||||
|
||||
/* check if job was triggered and, if
|
||||
so, if we have enough slots to run? */
|
||||
%if ("&&jobuid&jid"="0") and (&concurrency<&maxconcurrency) %then %do;
|
||||
|
||||
/* But only start if no issues detected so far */
|
||||
%if not ( &raise_err and &syscc ) %then %do;
|
||||
|
||||
%local jobname jobpath;
|
||||
%let jobname=%scan(&&job&jid,-1,/);
|
||||
%let jobpath=
|
||||
%substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
|
||||
|
||||
%put executing &jobpath/&jobname with paramstring &&jparams&jid;
|
||||
%mv_jobexecute(path=&jobpath
|
||||
,name=&jobname
|
||||
,paramstring=%superq(jparams&jid)
|
||||
,outds=&jdsapp
|
||||
)
|
||||
data &jdsapp;
|
||||
format jobparams $32767.;
|
||||
set &jdsapp(where=(method='GET' and rel='state'));
|
||||
jobparams=symget("jparams&jid");
|
||||
/* uri here has the /state suffix */
|
||||
uuid=scan(uri,-2,'/');
|
||||
call symputx("jobuid&jid",uuid,'l');
|
||||
run;
|
||||
proc append base=&jdsrunning data=&jdsapp;
|
||||
run;
|
||||
%let concurrency=%eval(&concurrency+1);
|
||||
/* sleep one second after every request to smooth the impact */
|
||||
data _null_;
|
||||
call sleep(1,1);
|
||||
run;
|
||||
|
||||
%end;
|
||||
%else %do; /* Job was skipped due to problems */
|
||||
|
||||
%put jobid &&job&jid in flow &fid skipped due to SYSCC (&syscc);
|
||||
%let completed = %eval(&completed+1);
|
||||
%let job&jid=0; /* Indicate job has finished */
|
||||
|
||||
%end;
|
||||
|
||||
%end;
|
||||
%end;
|
||||
%if &jid=&jcnt %then %do;
|
||||
/* we are at the end of the loop - check which jobs have finished */
|
||||
%mv_jobwaitfor(ANY,inds=&jdsrunning,outds=&jdswaitfor,outref=&outref
|
||||
,raise_err=&raise_err,mdebug=&mdebug)
|
||||
%local done;
|
||||
%let done=%mf_nobs(&jdswaitfor);
|
||||
%if &done>0 %then %do;
|
||||
%let completed=%eval(&completed+&done);
|
||||
%let concurrency=%eval(&concurrency-&done);
|
||||
data &jdsapp;
|
||||
set &jdswaitfor;
|
||||
flow_id=&&flow&fid;
|
||||
uuid=scan(uri,-1,'/');
|
||||
run;
|
||||
proc append base=&outds data=&jdsapp;
|
||||
run;
|
||||
%end;
|
||||
proc sql;
|
||||
delete from &jdsrunning
|
||||
where uuid in (select uuid from &outds
|
||||
where state in ('canceled','completed','failed')
|
||||
);
|
||||
|
||||
/* check if job was triggered and if so, if we have enough slots to run */
|
||||
%if "&&jobuid&jid"="0" and &concurrency<&maxconcurrency %then %do;
|
||||
%local jobname jobpath;
|
||||
%let jobname=%scan(&&job&jid,-1,/);
|
||||
%let jobpath=
|
||||
%substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
|
||||
%put executing &jobpath/&jobname with paramstring &&jparams&jid;
|
||||
%mv_jobexecute(path=&jobpath
|
||||
,name=&jobname
|
||||
,paramstring=%superq(jparams&jid)
|
||||
,outds=&jdsapp
|
||||
,mdebug=&mdebug
|
||||
)
|
||||
data &jdsapp;
|
||||
format jobparams $32767.;
|
||||
set &jdsapp(where=(method='GET' and rel='state'));
|
||||
jobparams=symget("jparams&jid");
|
||||
/* uri here has the /state suffix */
|
||||
uuid=scan(uri,-2,'/');
|
||||
call symputx("jobuid&jid",uuid,'l');
|
||||
run;
|
||||
proc append base=&jdsrunning data=&jdsapp;
|
||||
run;
|
||||
%let concurrency=%eval(&concurrency+1);
|
||||
/* sleep one second after every request to smooth the impact */
|
||||
data _null_;
|
||||
call sleep(1,1);
|
||||
run;
|
||||
/* loop again if jobs are left */
|
||||
%if &completed < &jcnt %then %do;
|
||||
%let jid=0;
|
||||
%put looping flow &fid again;
|
||||
%put &completed of &jcnt jobs completed, &concurrency jobs running;
|
||||
%end;
|
||||
%end;
|
||||
%end;
|
||||
%if &jid=&jcnt %then %do;
|
||||
/* we are at the end of the loop - time to see which jobs have finished */
|
||||
%mv_jobwaitfor(ANY
|
||||
,inds=&jdsrunning
|
||||
,outds=&jdswaitfor
|
||||
,outref=&outref
|
||||
,raise_err=&raise_err
|
||||
,mdebug=&mdebug
|
||||
)
|
||||
%local done;
|
||||
%let done=%mf_nobs(&jdswaitfor);
|
||||
%if &done>0 %then %do;
|
||||
%let completed=%eval(&completed+&done);
|
||||
%let concurrency=%eval(&concurrency-&done);
|
||||
data &jdsapp;
|
||||
set &jdswaitfor;
|
||||
flow_id=&&flow&fid;
|
||||
uuid=scan(uri,-1,'/');
|
||||
run;
|
||||
proc append base=&outds data=&jdsapp;
|
||||
run;
|
||||
%end;
|
||||
proc sql;
|
||||
delete from &jdsrunning
|
||||
where uuid in (select uuid from &outds
|
||||
where state in ('canceled','completed','failed')
|
||||
);
|
||||
|
||||
/* loop again if jobs are left */
|
||||
%if &completed < &jcnt %then %do;
|
||||
%let jid=0;
|
||||
%put looping flow &fid again - &completed of &jcnt jobs completed,
|
||||
&concurrency jobs running;
|
||||
%end;
|
||||
%end;
|
||||
%end;
|
||||
%else %do;
|
||||
|
||||
%put Flow &&flow&fid skipped due to SYSCC (&syscc);
|
||||
|
||||
%end;
|
||||
/* back up and execute the next flow */
|
||||
%end;
|
||||
|
||||
@@ -1,23 +1,11 @@
|
||||
/**
|
||||
@file
|
||||
@brief abort gracefully according to context
|
||||
@details Do not use directly! See bottom of explanation for details.
|
||||
@brief to be deprecated
|
||||
@details We will deprecate this macro in 2022
|
||||
|
||||
Configures an abort mechanism according to site specific policies or the
|
||||
particulars of an environment. For instance, can stream custom
|
||||
results back to the client in an STP Web App context, or completely stop
|
||||
in the case of a batch run.
|
||||
As you can see, it's not a macro function.
|
||||
|
||||
For the sharp eyed readers - this is no longer a macro function!! It became
|
||||
a macro procedure during a project and now it's kinda stuck that way until
|
||||
that project is updated (if it's ever updated). In the meantime we created
|
||||
`mp_abort` which is just a wrapper for this one, and so we recomend you use
|
||||
that for forwards compatibility reasons.
|
||||
|
||||
@param mac= to contain the name of the calling macro
|
||||
@param type= deprecated. Not used.
|
||||
@param msg= message to be returned
|
||||
@param iftrue= supply a condition under which the macro should be executed.
|
||||
Use mp_abort.sas instead.
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
|
||||
@@ -4,13 +4,24 @@
|
||||
@details Configures an abort mechanism according to site specific policies or
|
||||
the particulars of an environment. For instance, can stream custom
|
||||
results back to the client in an STP Web App context, or completely stop
|
||||
in the case of a batch run.
|
||||
in the case of a batch run. For STP sessions
|
||||
|
||||
The method used varies according to the context. Important points:
|
||||
|
||||
@li should not use endsas or abort cancel in 9.4m3 environments as this can
|
||||
cause hung multibridge sessions and result in a frozen STP server
|
||||
@li should not use endsas in viya 3.5 as this destroys the session and cannot
|
||||
fetch results (although both mv_getjoblog.sas and the @sasjs/adapter will
|
||||
recognise this and fetch the log of the parent session instead)
|
||||
@li STP environments must finish cleanly to avoid the log being sent to
|
||||
_webout. To assist with this, we also run stpsrvset('program error', 0)
|
||||
and set SYSCC=0. For 9.4m3 we take a unique approach - we open a macro
|
||||
but don't close it! This provides a graceful abort, EXCEPT when called
|
||||
called within a %include within a macro (and that macro contains additional
|
||||
logic). See mp_abort.test.nofix.sas for the example case.
|
||||
If you know of another way to gracefully abort a 9.4m3 STP session, we'd
|
||||
love to hear about it!
|
||||
|
||||
Using SAS Abort Cancel mechanisms can cause hung sessions in some Stored
|
||||
Process environments. This macro takes a unique approach - we set the SAS
|
||||
syscc to 0, run `stpsrvset('program error', 0)` (if SAS 9) and then - we open
|
||||
a macro but don't close it! This provides a graceful abort for SAS web
|
||||
services in all web enabled environments.
|
||||
|
||||
@param mac= to contain the name of the calling macro
|
||||
@param msg= message to be returned
|
||||
@@ -24,6 +35,8 @@
|
||||
%macro mp_abort(mac=mp_abort.sas, type=, msg=, iftrue=%str(1=1)
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%global sysprocessmode sysprocessname;
|
||||
|
||||
%if not(%eval(%unquote(&iftrue))) %then %return;
|
||||
|
||||
%put NOTE: /// mp_abort macro executing //;
|
||||
@@ -31,9 +44,7 @@
|
||||
%put NOTE - &msg;
|
||||
|
||||
/* Stored Process Server web app context */
|
||||
%if %symexist(_metaperson)
|
||||
or (%symexist(SYSPROCESSNAME) and "&SYSPROCESSNAME"="Compute Server" )
|
||||
%then %do;
|
||||
%if %symexist(_metaperson) or "&SYSPROCESSNAME "="Compute Server " %then %do;
|
||||
options obs=max replace nosyntaxcheck mprint;
|
||||
/* extract log errs / warns, if exist */
|
||||
%local logloc logline;
|
||||
@@ -63,7 +74,7 @@
|
||||
input;
|
||||
i=1;
|
||||
stoploop=0;
|
||||
if _n_ ge &logline-5 and stoploop=0 then do until (i>12);
|
||||
if _n_ ge &logline-15 and stoploop=0 then do until (i>22);
|
||||
call symputx('logmsg',catx('\n',symget('logmsg'),_infile_));
|
||||
input;
|
||||
i+1;
|
||||
@@ -128,32 +139,59 @@
|
||||
if debug ge '"131"' then put '>>weboutEND<<';
|
||||
run;
|
||||
|
||||
%if %symexist(_metaport) %then %do;
|
||||
%put _all_;
|
||||
|
||||
%if "&sysprocessmode " = "SAS Stored Process Server " %then %do;
|
||||
data _null_;
|
||||
if symexist('sysprocessmode') then
|
||||
if symget("sysprocessmode")="SAS Stored Process Server" then do;
|
||||
rc=stpsrvset('program error', 0);
|
||||
call symputx("syscc",0,"g");
|
||||
end;
|
||||
putlog 'stpsrvset program error and syscc';
|
||||
rc=stpsrvset('program error', 0);
|
||||
call symputx("syscc",0,"g");
|
||||
run;
|
||||
%if "%substr(&sysvlong.xxxxxxxxx,1,9)" ne "9.04.01M3" %then %do;
|
||||
%put NOTE: Ending SAS session due to:;
|
||||
%put NOTE- &msg;
|
||||
endsas;
|
||||
%end;
|
||||
%end;
|
||||
%else %if "&sysprocessmode " = "SAS Compute Server " %then %do;
|
||||
/* endsas kills the session making it harder to fetch results */
|
||||
data _null_;
|
||||
syswarningtext=symget('syswarningtext');
|
||||
syserrortext=symget('syserrortext');
|
||||
abort_msg=symget('msg');
|
||||
syscc=symget('syscc');
|
||||
sysuserid=symget('sysuserid');
|
||||
iftrue=symget('iftrue');
|
||||
put (_all_)(/=);
|
||||
abort cancel nolist;
|
||||
run;
|
||||
%end;
|
||||
/**
|
||||
* endsas is reliable but kills some deployments.
|
||||
* Abort variants are ungraceful (non zero return code)
|
||||
* This approach lets SAS run silently until the end :-)
|
||||
*/
|
||||
%put _all_;
|
||||
filename skip temp;
|
||||
data _null_;
|
||||
file skip;
|
||||
put '%macro skip(); %macro skippy();';
|
||||
run;
|
||||
%inc skip;
|
||||
%else %if "%substr(&sysvlong.xxxxxxxxx,1,9)" = "9.04.01M3" %then %do;
|
||||
/**
|
||||
* endsas kills 9.4m3 deployments by orphaning multibridges.
|
||||
* Abort variants are ungraceful (non zero return code)
|
||||
* This approach lets SAS run silently until the end :-)
|
||||
* Caution - fails when called within a %include within a macro
|
||||
* See tests/mp_abort.test.1 for an example case.
|
||||
*/
|
||||
filename skip temp;
|
||||
data _null_;
|
||||
file skip;
|
||||
put '%macro skip();';
|
||||
comment '%mend skip; -> fix lint ';
|
||||
put '%macro skippy();';
|
||||
comment '%mend skippy; -> fix lint ';
|
||||
run;
|
||||
%inc skip;
|
||||
%end;
|
||||
%else %do;
|
||||
%abort cancel;
|
||||
%end;
|
||||
%end;
|
||||
%else %do;
|
||||
%put _all_;
|
||||
%abort cancel;
|
||||
%end;
|
||||
%mend;
|
||||
%mend mp_abort;
|
||||
|
||||
/** @endcond */
|
||||
@@ -1,20 +1,24 @@
|
||||
/**
|
||||
@file
|
||||
@brief Drops tables / views (if they exist) without warnings in the log
|
||||
@details
|
||||
@details Useful for dropping tables when you're not sure they exist, or if
|
||||
you are not sure whether they are a dataset or view. Also efficient for
|
||||
dropping multiple tables / views.
|
||||
|
||||
Example usage:
|
||||
|
||||
proc sql;
|
||||
create table data1 as select * from sashelp.class;
|
||||
create view view2 as select * from sashelp.class;
|
||||
%mp_dropmembers(list=data1 view2)
|
||||
%mp_dropmembers(data1 view2, libref=WORK)
|
||||
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mf_isblank.sas
|
||||
|
||||
|
||||
@param list space separated list of datasets / views
|
||||
@param libref= can only drop from a single library at a time
|
||||
@param list space separated list of datasets / views, WITHOUT libref
|
||||
@param libref= (WORK) Note - you can only drop from a single library at a time
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
@@ -35,4 +39,4 @@
|
||||
delete &list;
|
||||
delete &list /mtype=view;
|
||||
run;
|
||||
%mend;
|
||||
%mend mp_dropmembers;
|
||||
@@ -33,19 +33,19 @@
|
||||
@returns The &outds table containing any bad rows, plus a REASON_CD column.
|
||||
|
||||
@param [in] inds The table to be checked, with the format above
|
||||
@param [in] targetds= The target dataset against which to verify VARIABLE_NM
|
||||
@param [in] targetds= The target dataset against which to verify VARIABLE_NM.
|
||||
This must be available (ie, the library must be assigned).
|
||||
@param [out] abort= (YES) If YES will call mp_abort.sas on any exceptions
|
||||
@param [out] outds= The output table, which is a copy of the &inds. table
|
||||
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||
processes should check this table (and return code) before continuing.
|
||||
plus a REASON_CD column, containing only bad records. If bad records found,
|
||||
the SYSCC value will be set to 1008 (general data problem). Downstream
|
||||
processes should check this table (and return code) before continuing.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_abort.sas
|
||||
@li mf_getuniquefileref.sas
|
||||
@li mf_getvarlist.sas
|
||||
@li mf_getvartype.sas
|
||||
@li mf_nobs.sas
|
||||
@li mp_filtergenerate.sas
|
||||
@li mp_filtervalidate.sas
|
||||
|
||||
@@ -85,41 +85,52 @@
|
||||
* quotes, commas, periods and spaces.
|
||||
* Only numeric values should remain
|
||||
*/
|
||||
%local reason_cd;
|
||||
%local reason_cd nobs;
|
||||
%let nobs=0;
|
||||
data &outds;
|
||||
/*length GROUP_LOGIC SUBGROUP_LOGIC $3 SUBGROUP_ID 8 VARIABLE_NM $32
|
||||
OPERATOR_NM $10 RAW_VALUE $4000;*/
|
||||
set &inds;
|
||||
length reason_cd $32;
|
||||
length reason_cd $4032;
|
||||
|
||||
/* closed list checks */
|
||||
if GROUP_LOGIC not in ('AND','OR') then do;
|
||||
REASON_CD='GROUP_LOGIC should be either AND or OR';
|
||||
REASON_CD='GROUP_LOGIC should be AND/OR, not:'!!cats(GROUP_LOGIC);
|
||||
putlog REASON_CD= GROUP_LOGIC=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if SUBGROUP_LOGIC not in ('AND','OR') then do;
|
||||
REASON_CD='SUBGROUP_LOGIC should be either AND or OR';
|
||||
REASON_CD='SUBGROUP_LOGIC should be AND/OR, not:'!!cats(SUBGROUP_LOGIC);
|
||||
putlog REASON_CD= SUBGROUP_LOGIC=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if mod(SUBGROUP_ID,1) ne 0 then do;
|
||||
REASON_CD='SUBGROUP_ID should be integer';
|
||||
REASON_CD='SUBGROUP_ID should be integer, not '!!left(subgroup_id);
|
||||
putlog REASON_CD= SUBGROUP_ID=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if upcase(VARIABLE_NM) not in
|
||||
(%upcase(%mf_getvarlist(&targetds,dlm=%str(,),quote=SINGLE)))
|
||||
then do;
|
||||
REASON_CD="VARIABLE_NM not in &targetds";
|
||||
REASON_CD="Variable "!!cats(variable_nm)!!" not in &targetds";
|
||||
putlog REASON_CD= VARIABLE_NM=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
if OPERATOR_NM not in
|
||||
('=','>','<','<=','>=','BETWEEN','IN','NOT IN','NE','CONTAINS')
|
||||
then do;
|
||||
REASON_CD='Invalid OPERATOR_NM';
|
||||
REASON_CD='Invalid OPERATOR_NM: '!!left(OPERATOR_NM);
|
||||
putlog REASON_CD= OPERATOR_NM=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
|
||||
@@ -129,8 +140,10 @@ data &outds;
|
||||
if substr(raw_value,1,1) ne '('
|
||||
or substr(cats(reverse(raw_value)),1,1) ne ')'
|
||||
then do;
|
||||
REASON_CD='Missing brackets in RAW_VALUE';
|
||||
REASON_CD='Missing start/end bracket in RAW_VALUE';
|
||||
putlog REASON_CD= OPERATOR_NM= raw_value= raw_value1= ;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
else raw_value1=substr(raw_value,2,max(length(raw_value)-2,0));
|
||||
@@ -151,25 +164,27 @@ data &outds;
|
||||
/* output records that contain values other than digits and spaces */
|
||||
if notdigit(compress(raw_value3,' '))>0 then do;
|
||||
putlog raw_value3= $hex32.;
|
||||
REASON_CD='Invalid RAW_VALUE';
|
||||
REASON_CD=cats('Invalid RAW_VALUE:',raw_value);
|
||||
putlog REASON_CD= raw_value= raw_value1= raw_value2= raw_value3=;
|
||||
call symputx('reason_cd',reason_cd,'l');
|
||||
call symputx('nobs',_n_,'l');
|
||||
output;
|
||||
end;
|
||||
|
||||
run;
|
||||
|
||||
|
||||
data _null_;
|
||||
set &outds;
|
||||
call symputx('REASON_CD',reason_cd,'l');
|
||||
stop;
|
||||
set &outds end=last;
|
||||
putlog (_all_)(=);
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&abort=YES and %mf_nobs(&outds)>0),
|
||||
%mp_abort(iftrue=(&abort=YES and &nobs>0),
|
||||
mac=&sysmacroname,
|
||||
msg=%str(Filter issues in &inds, reason: &reason_cd, details in &outds)
|
||||
msg=%str(Data issue: %superq(reason_cd))
|
||||
)
|
||||
|
||||
%if %mf_nobs(&outds)>0 %then %do;
|
||||
%if &nobs>0 %then %do;
|
||||
%let syscc=1008;
|
||||
%return;
|
||||
%end;
|
||||
|
||||
@@ -95,7 +95,8 @@ filename &fref1 clear;
|
||||
run;
|
||||
%mp_abort(
|
||||
mac=&sysmacroname,
|
||||
msg=%str(Filter issues in &inref: %quote(&reason_cd))
|
||||
msg=%str(Filter validation issues. ERR=%superq(SYSERRORTEXT)
|
||||
, WARN=%superq(SYSWARNINGTEXT) )
|
||||
)
|
||||
%end;
|
||||
%let syscc=1008;
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
)/*/STORE SOURCE*/;
|
||||
%put output location=&jref;
|
||||
%if &action=OPEN %then %do;
|
||||
OPTIONS NOBOMFILE;
|
||||
options nobomfile;
|
||||
data _null_;file &jref encoding='utf-8';
|
||||
put '{"START_DTTM" : "' "%sysfunc(datetime(),datetime20.3)" '"';
|
||||
run;
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
|
||||
%mp_searchcols(libs=sashelp work, cols=name sex age)
|
||||
|
||||
@param libs=
|
||||
@param libs=(SASHELP) Space separated list of libraries to search for columns
|
||||
@param cols= Space separated list of column names to search for (not case
|
||||
sensitive)
|
||||
@param outds=(mp_searchcols) the table to create with the results. Will have
|
||||
one line per table match.
|
||||
@param match=(ANY) The match type. Valid values:
|
||||
@li ANY - The table contains at least one of the columns
|
||||
@li WILD - The table contains a column with a name that partially matches
|
||||
|
||||
@version 9.2
|
||||
@author Allan Bowe
|
||||
**/
|
||||
@@ -17,6 +25,7 @@
|
||||
%macro mp_searchcols(libs=sashelp
|
||||
,cols=
|
||||
,outds=mp_searchcols
|
||||
,match=ANY
|
||||
)/*/STORE SOURCE*/;
|
||||
|
||||
%put &sysmacroname process began at %sysfunc(datetime(),datetime19.);
|
||||
@@ -38,8 +47,10 @@ create table _data_ as
|
||||
%end;
|
||||
order by 1,2,3;
|
||||
|
||||
%local tempds;
|
||||
%let tempds=&syslast;
|
||||
data &outds;
|
||||
set &syslast;
|
||||
set &tempds;
|
||||
length cols matchcols $32767;
|
||||
cols=upcase(symget('cols'));
|
||||
colcount=countw(cols);
|
||||
@@ -53,10 +64,29 @@ data &outds;
|
||||
retain matchcols;
|
||||
matchcols='';
|
||||
end;
|
||||
%if &match=ANY %then %do;
|
||||
if findw(cols,name,,'spit') then do;
|
||||
sumcols+1;
|
||||
matchcols=cats(matchcols)!!' '!!cats(name);
|
||||
end;
|
||||
%end;
|
||||
%else %if &match=WILD %then %do;
|
||||
if _n_=1 then do;
|
||||
retain wcount;
|
||||
wcount=countw(cols);
|
||||
drop wcount;
|
||||
end;
|
||||
do i=1 to wcount;
|
||||
length curword $32;
|
||||
curword=scan(cols,i,' ');
|
||||
drop curword;
|
||||
if index(name,cats(curword)) then do;
|
||||
sumcols+1;
|
||||
matchcols=cats(matchcols)!!' '!!cats(curword);
|
||||
end;
|
||||
end;
|
||||
%end;
|
||||
|
||||
if last.memname then do;
|
||||
if sumcols>0 then output;
|
||||
if sumcols=colcount then putlog "Full Match: " libname memname;
|
||||
@@ -66,6 +96,8 @@ run;
|
||||
|
||||
proc sort; by descending sumcols memname libname; run;
|
||||
|
||||
proc sql;
|
||||
drop table &tempds;
|
||||
%put &sysmacroname process finished at %sysfunc(datetime(),datetime19.);
|
||||
|
||||
%mend;
|
||||
%mend mp_searchcols;
|
||||
@@ -90,7 +90,7 @@ data _null_;
|
||||
put ')/*/STORE SOURCE*/; ';
|
||||
put '%put output location=&jref; ';
|
||||
put '%if &action=OPEN %then %do; ';
|
||||
put ' OPTIONS NOBOMFILE; ';
|
||||
put ' options nobomfile; ';
|
||||
put ' data _null_;file &jref encoding=''utf-8''; ';
|
||||
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
|
||||
put ' run; ';
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
#### &prefix.added
|
||||
|name:$32.|metaID:$17.|SAStabName:$32.|
|
||||
|---|---|---|
|
||||
|||DATA1|
|
||||
| | |DATA1|
|
||||
|
||||
#### &prefix.deleted
|
||||
|name:$32.|metaID:$17.|SAStabName:$32.|
|
||||
@@ -59,7 +59,7 @@
|
||||
|tabName:$32.|tabMetaID:$17.|SAStabName:$32.|metaName:$32.|metaID:$17.|sasname:$32.|metaType:$16.|change:$64.|
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|TABLE2|A5XLSNXI.BK0001HN|TABLE2|c|A5XLSNXI.BM000MA9|c|Column|Deleted|
|
||||
||||d||d|Column|Added|
|
||||
| | | |d| |d|Column|Added|
|
||||
|
||||
#### &prefix.meta
|
||||
|Label1:$28.|cValue1:$1.|nValue1:D12.3|
|
||||
@@ -80,9 +80,8 @@
|
||||
such as dangling metadata, embedded passwords, security issues and more.
|
||||
|
||||
@param [in] libname= the metadata name of the library to be compared
|
||||
@param [out] outlib= The output library in which to store the output tables.
|
||||
Default=WORK.
|
||||
@param [out] prefix The prefix for the four tables created. Default=metadiff.
|
||||
@param [out] outlib=(work) The library in which to store the output tables.
|
||||
@param [out] prefix=(metadiff) The prefix for the four tables created.
|
||||
|
||||
@version 9.3
|
||||
@author Allan Bowe
|
||||
|
||||
36
tests/base/mp_abort.test.nofix.sas
Normal file
36
tests/base/mp_abort.test.nofix.sas
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mp_abort macro
|
||||
@details This is an unfixed problem with mp_abort when using the
|
||||
'unclosed macro' technique. This is only relevant for 9.4m3 environments,
|
||||
which can suffer from hung multibridge sessions from %abort and endsas.
|
||||
|
||||
The issue is that when called within a macro, within a %include, AND that
|
||||
macro contains subsequent logic, the service does not end cleanly - rather,
|
||||
we see:
|
||||
|
||||
ERROR: %EVAL function has no expression to evaluate, or %IF statement has no condition.
|
||||
ERROR: The macro TEST will stop executing.
|
||||
|
||||
We are not able to test this without a 9.4m3 environment, it is marked as
|
||||
nofix.
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_abort.sas
|
||||
@li mp_assert.sas
|
||||
|
||||
**/
|
||||
|
||||
%macro test();
|
||||
|
||||
filename blah temp;
|
||||
data _null_;
|
||||
file blah;
|
||||
put '%mp_abort();';
|
||||
run;
|
||||
%inc blah;
|
||||
|
||||
%if 1=1 %then %put Houston - we have a problem here;
|
||||
%mend test;
|
||||
|
||||
%test()
|
||||
50
tests/base/mp_searchcols.test.sas
Normal file
50
tests/base/mp_searchcols.test.sas
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mp_searchcols.sas
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_searchcols.sas
|
||||
@li mp_assertdsobs.sas
|
||||
|
||||
|
||||
**/
|
||||
|
||||
|
||||
/** Test 1 - full col match */
|
||||
data example1;
|
||||
var1=1;
|
||||
var2=2;
|
||||
var3=3;
|
||||
data example2;
|
||||
var1=1;
|
||||
var2=2;
|
||||
data example3;
|
||||
var2=2;
|
||||
var3=3;
|
||||
data example4;
|
||||
matchmehere=1;
|
||||
data example5;
|
||||
hereyoucan_matchme_also=1;
|
||||
data example6;
|
||||
do_not_forget_me=1;
|
||||
data example7;
|
||||
we_shall_not_forget=1;
|
||||
run;
|
||||
|
||||
%mp_searchcols(libs=work,cols=var1 var2,outds=testme)
|
||||
|
||||
%mp_assertdsobs(work.testme,
|
||||
desc=Test1 - check exact variables are found,
|
||||
test=EQUALS 3,
|
||||
outds=work.test_results
|
||||
)
|
||||
|
||||
/* test 2 - wildcard match */
|
||||
|
||||
%mp_searchcols(libs=work,cols=matchme forget,match=WILD, outds=testme2)
|
||||
|
||||
%mp_assertdsobs(work.testme2,
|
||||
desc=Test1 - check fuzzy matches are found,
|
||||
test=EQUALS 4,
|
||||
outds=work.test_results
|
||||
)
|
||||
70
tests/viya/mv_getjoblog.test.sas
Normal file
70
tests/viya/mv_getjoblog.test.sas
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mv_createwebservice macro
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mv_createjob.sas
|
||||
@li mv_jobexecute.sas
|
||||
@li mv_jobwaitfor.sas
|
||||
@li mv_getjoblog.sas
|
||||
|
||||
**/
|
||||
|
||||
/**
|
||||
* Test Case 1
|
||||
*/
|
||||
|
||||
/* create a service */
|
||||
filename testref temp;
|
||||
data _null_;
|
||||
file testref;
|
||||
put 'data;run;';
|
||||
put 'endsas;';
|
||||
run;
|
||||
%mv_createjob(
|
||||
path=&mcTestAppLoc/jobs/temp,
|
||||
code=testref,
|
||||
name=testjob
|
||||
)
|
||||
|
||||
%* Execute it;
|
||||
%mv_jobexecute(
|
||||
path=&mcTestAppLoc/jobs/temp,
|
||||
name=testjob,
|
||||
outds=work.info
|
||||
)
|
||||
|
||||
%* Wait for it to finish;
|
||||
data work.info;
|
||||
set work.info;
|
||||
where method='GET' and rel='state';
|
||||
run;
|
||||
%mv_jobwaitfor(ALL,inds=work.info,outds=work.jobstates)
|
||||
|
||||
%* and grab the uri;
|
||||
data _null_;
|
||||
set work.jobstates;
|
||||
call symputx('uri',uri);
|
||||
run;
|
||||
|
||||
%* Finally, fetch the log;
|
||||
%mv_getjoblog(uri=%str(&uri),outref=mylog)
|
||||
|
||||
|
||||
data _null_;
|
||||
infile mylog end=eof;
|
||||
input;
|
||||
putlog _infile_;
|
||||
retain found 0;
|
||||
if index(_infile_,'endsas;') then do;
|
||||
found=1;
|
||||
call symputx('found',found);
|
||||
end;
|
||||
else if eof and found ne 1 then call symputx('found',0);
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(%str(&found)=1),
|
||||
desc=Check if the log was still fetched even though endsas was submitted
|
||||
)
|
||||
78
tests/viya/mv_jobflow.test.1.sas
Normal file
78
tests/viya/mv_jobflow.test.1.sas
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
|
||||
@file
|
||||
@brief Testing mv_jobflow macro
|
||||
@details One of the remote jobs aborts with syscc>0 - test to
|
||||
make sure this comes back to the calling session
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mv_createjob.sas
|
||||
@li mv_jobflow.sas
|
||||
|
||||
**/
|
||||
|
||||
/**
|
||||
* Test Case 1
|
||||
*/
|
||||
|
||||
filename testprog temp;
|
||||
data _null_;
|
||||
file testprog;
|
||||
put '%put this is job: &_program;'
|
||||
/ '%put this was run in flow &flow_id;'
|
||||
/ 'data ;'
|
||||
/ ' rval=rand("uniform");'
|
||||
/ ' rand=rval*¯ovar1;'
|
||||
/ ' do x=1 to rand;'
|
||||
/ ' y=rand*¯ovar2;'
|
||||
/ ' if (rval>0.50) then abort;'
|
||||
/ ' else output;'
|
||||
/ ' end;'
|
||||
/ 'run;'
|
||||
;
|
||||
run;
|
||||
|
||||
%mv_createjob(path=/Public/temp,name=demo1,code=testprog)
|
||||
%mv_createjob(path=/Public/temp,name=demo2,code=testprog)
|
||||
|
||||
data work.inputjobs;
|
||||
_contextName='SAS Job Execution compute context';
|
||||
do flow_id=1 to 2;
|
||||
do i=1 to 4;
|
||||
_program='/Public/temp/demo1';
|
||||
macrovar1=10*i;
|
||||
macrovar2=4*i;
|
||||
output;
|
||||
i+1;
|
||||
_program='/Public/temp/demo2';
|
||||
macrovar1=40*i;
|
||||
macrovar2=44*i;
|
||||
output;
|
||||
end;
|
||||
end;
|
||||
run;
|
||||
|
||||
* Trigger the flow ;
|
||||
|
||||
%put NOTE: &=syscc;
|
||||
|
||||
%mv_jobflow(inds=work.inputjobs
|
||||
,maxconcurrency=2
|
||||
,outds=work.results
|
||||
,outref=myjoblog
|
||||
,raise_err=1
|
||||
,mdebug=1
|
||||
)
|
||||
|
||||
%put NOTE: &=syscc;
|
||||
|
||||
data _null_;
|
||||
infile myjoblog;
|
||||
input; put _infile_;
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&syscc ne 0),
|
||||
desc=Check that non zero return code is returned if called job fails
|
||||
)
|
||||
74
tests/viya/mv_jobflow.test.2.sas
Normal file
74
tests/viya/mv_jobflow.test.2.sas
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
@file
|
||||
@brief Testing mv_jobflow macro
|
||||
@details All jobs complete successfully with syscc = 0 - test to
|
||||
make sure this comes back to the calling session
|
||||
|
||||
<h4> SAS Macros </h4>
|
||||
@li mp_assert.sas
|
||||
@li mv_createjob.sas
|
||||
@li mv_jobflow.sas
|
||||
**/
|
||||
|
||||
/**
|
||||
* Test Case 1
|
||||
*/
|
||||
filename testprog temp;
|
||||
data _null_;
|
||||
file testprog;
|
||||
put '%put this is job: &_program;'
|
||||
/ '%put this was run in flow &flow_id;'
|
||||
/ 'data ;'
|
||||
/ ' rval=rand("uniform");'
|
||||
/ ' rand=rval*¯ovar1;'
|
||||
/ ' do x=1 to rand;'
|
||||
/ ' y=rand*¯ovar2;'
|
||||
/ ' output;'
|
||||
/ ' end;'
|
||||
/ 'run;'
|
||||
;
|
||||
run;
|
||||
|
||||
%mv_createjob(path=/Public/temp,name=demo1,code=testprog)
|
||||
%mv_createjob(path=/Public/temp,name=demo2,code=testprog)
|
||||
|
||||
data work.inputjobs;
|
||||
_contextName='SAS Job Execution compute context';
|
||||
do flow_id=1 to 2;
|
||||
do i=1 to 4;
|
||||
_program='/Public/temp/demo1';
|
||||
macrovar1=10*i;
|
||||
macrovar2=4*i;
|
||||
output;
|
||||
i+1;
|
||||
_program='/Public/temp/demo2';
|
||||
macrovar1=40*i;
|
||||
macrovar2=44*i;
|
||||
output;
|
||||
end;
|
||||
end;
|
||||
run;
|
||||
|
||||
* Trigger the flow ;
|
||||
|
||||
%put NOTE: &=syscc;
|
||||
|
||||
%mv_jobflow(inds=work.inputjobs
|
||||
,maxconcurrency=2
|
||||
,outds=work.results
|
||||
,outref=myjoblog
|
||||
,raise_err=1
|
||||
,mdebug=1
|
||||
)
|
||||
|
||||
%put NOTE: &=syscc;
|
||||
|
||||
data _null_;
|
||||
infile myjoblog;
|
||||
input; put _infile_;
|
||||
run;
|
||||
|
||||
%mp_assert(
|
||||
iftrue=(&syscc eq 0),
|
||||
desc=Check that a zero return code is returned if no called job fails
|
||||
)
|
||||
@@ -241,7 +241,7 @@ data _null_;
|
||||
put ')/*/STORE SOURCE*/; ';
|
||||
put '%put output location=&jref; ';
|
||||
put '%if &action=OPEN %then %do; ';
|
||||
put ' OPTIONS NOBOMFILE; ';
|
||||
put ' options nobomfile; ';
|
||||
put ' data _null_;file &jref encoding=''utf-8''; ';
|
||||
put ' put ''{"START_DTTM" : "'' "%sysfunc(datetime(),datetime20.3)" ''"''; ';
|
||||
put ' run; ';
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
/**
|
||||
@file
|
||||
@brief Extract the log from a completed SAS Viya Job
|
||||
@details Extracts log from a Viya job and writes it out to a fileref
|
||||
@details Extracts log from a Viya job and writes it out to a fileref.
|
||||
|
||||
To query the job, you need the URI. Sample code for achieving this
|
||||
is provided below.
|
||||
|
||||
## Example
|
||||
|
||||
First, compile the macros:
|
||||
|
||||
%* First, compile the macros;
|
||||
filename mc url
|
||||
"https://raw.githubusercontent.com/sasjs/core/main/all.sas";
|
||||
%inc mc;
|
||||
|
||||
Next, create a job (in this case, a web service):
|
||||
|
||||
%* Next, create a job (in this case, a web service);
|
||||
filename ft15f001 temp;
|
||||
parmcards4;
|
||||
data ;
|
||||
@@ -31,28 +29,40 @@
|
||||
;;;;
|
||||
%mv_createwebservice(path=/Public/temp,name=demo)
|
||||
|
||||
Execute it:
|
||||
|
||||
%* Execute it;
|
||||
%mv_jobexecute(path=/Public/temp
|
||||
,name=demo
|
||||
,outds=work.info
|
||||
)
|
||||
|
||||
Wait for it to finish, and grab the uri:
|
||||
|
||||
data _null_;
|
||||
%* Wait for it to finish;
|
||||
data work.info;
|
||||
set work.info;
|
||||
if method='GET' and rel='self';
|
||||
where method='GET' and rel='state';
|
||||
run;
|
||||
%mv_jobwaitfor(ALL,inds=work.info,outds=work.jobstates)
|
||||
|
||||
%* and grab the uri;
|
||||
data _null_;
|
||||
set work.jobstates;
|
||||
call symputx('uri',uri);
|
||||
run;
|
||||
|
||||
Finally, fetch the log:
|
||||
|
||||
%* Finally, fetch the log;
|
||||
%mv_getjoblog(uri=&uri,outref=mylog)
|
||||
|
||||
This macro is used by the mv_jobwaitfor.sas macro, which is generally a more
|
||||
convenient way to wait for the job to finish before fetching the log.
|
||||
|
||||
If the remote session calls `endsas` then it is not possible to get the log
|
||||
from the provided uri, and so the log from the parent session is fetched
|
||||
instead. This happens for a 400 response, eg below:
|
||||
|
||||
ErrorResponse[version=2,status=400,err=5113,id=,message=The session
|
||||
requested is currently in a failed or stopped state.,detail=[path:
|
||||
/compute/sessions/LONGURI-ses0006/jobs/LONGURI/log/content, traceId: 63
|
||||
51aa617d01fd2b],remediation=Correct the errors in the session request,
|
||||
and create a new session.,targetUri=<null>,errors=[],links=[]]
|
||||
|
||||
@param [in] access_token_var= The global macro variable to contain the access
|
||||
token
|
||||
@@ -64,7 +74,7 @@
|
||||
if a SASStudioV session else authorization_code. Default option.
|
||||
@li sas_services - will use oauth_bearer=sas_services.
|
||||
@param [in] uri= The uri of the running job for which to fetch the status,
|
||||
in the format `/jobExecution/jobs/$UUID/state` (unquoted).
|
||||
in the format `/jobExecution/jobs/$UUID` (unquoted).
|
||||
@param [out] outref= The output fileref to which to APPEND the log (is always
|
||||
appended).
|
||||
|
||||
@@ -122,7 +132,7 @@ data _null_;
|
||||
call symputx('errflg',1);
|
||||
call symputx('errmsg',
|
||||
"URI should be in format /jobExecution/jobs/$$$$UUID$$$$"
|
||||
!!" but is actually like: &uri",'l');
|
||||
!!" but is actually like:"!!uri,'l');
|
||||
end;
|
||||
run;
|
||||
|
||||
@@ -155,6 +165,10 @@ proc http method='GET' out=&fname1 &oauth_bearer
|
||||
%end;
|
||||
;
|
||||
run;
|
||||
%if &mdebug=1 %then %do;
|
||||
%put &sysmacroname: fetching log loc from &uri;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
|
||||
%do;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
@@ -192,43 +206,74 @@ run;
|
||||
data _null_;
|
||||
infile &fname2;
|
||||
input;
|
||||
uri=_infile_;
|
||||
uri=cats(_infile_);
|
||||
if length(uri)<12 then do;
|
||||
call symputx('errflg',1);
|
||||
call symputx('errmsg',"URI is invalid (too short) - '&uri'",'l');
|
||||
end;
|
||||
if scan(uri,1) ne 'files' or scan(uri,2) ne 'files' then do;
|
||||
else if (scan(uri,1,'/') ne 'compute' or scan(uri,2,'/') ne 'sessions')
|
||||
and (scan(uri,1,'/') ne 'files' or scan(uri,2,'/') ne 'files')
|
||||
then do;
|
||||
call symputx('errflg',1);
|
||||
call symputx('errmsg',
|
||||
"URI should be in format /files/files/$$$$UUID$$$$"
|
||||
!!" but is actually like: &uri",'l');
|
||||
"URI should be in format /compute/sessions/$$$$UUID$$$$/jobs/$$$$UUID$$$$"
|
||||
!!" or /files/files/$$$$UUID$$$$"
|
||||
!!" but is actually like:"!!uri,'l');
|
||||
end;
|
||||
else do;
|
||||
call symputx('errflg',0,'l');
|
||||
call symputx('logloc',uri,'l');
|
||||
end;
|
||||
call symputx('errflg',0,'l');
|
||||
call symputx('logloc',uri,'l');
|
||||
run;
|
||||
|
||||
%mp_abort(iftrue=(&errflg=1)
|
||||
%mp_abort(iftrue=(%str(&errflg)=1)
|
||||
,mac=&sysmacroname
|
||||
,msg=%str(&errmsg)
|
||||
)
|
||||
|
||||
/* we have a log uri - now fetch the log */
|
||||
%&dbg.put &sysmacroname: querying &base_uri&logloc/content;
|
||||
proc http method='GET' out=&fname1 &oauth_bearer
|
||||
url="&base_uri&logloc/content";
|
||||
url="&base_uri&logloc/content?limit=10000";
|
||||
headers
|
||||
%if &grant_type=authorization_code %then %do;
|
||||
"Authorization"="Bearer &&&access_token_var"
|
||||
%end;
|
||||
;
|
||||
run;
|
||||
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201 %then
|
||||
%do;
|
||||
|
||||
%if &mdebug=1 %then %do;
|
||||
%put &sysmacroname: fetching log content from &base_uri&logloc/content;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
|
||||
%if &SYS_PROCHTTP_STATUS_CODE=400 %then %do;
|
||||
/* fetch log from parent session */
|
||||
%let logloc=%substr(&logloc,1,%index(&logloc,%str(/jobs/))-1);
|
||||
%&dbg.put &sysmacroname: Now querying &base_uri&logloc/log/content;
|
||||
proc http method='GET' out=&fname1 &oauth_bearer
|
||||
url="&base_uri&logloc/log/content?limit=10000";
|
||||
headers
|
||||
%if &grant_type=authorization_code %then %do;
|
||||
"Authorization"="Bearer &&&access_token_var"
|
||||
%end;
|
||||
;
|
||||
run;
|
||||
%if &mdebug=1 %then %do;
|
||||
%put &sysmacroname: fetching log content from &base_uri&logloc/log/content;
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
%end;
|
||||
|
||||
%if &SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201
|
||||
%then %do;
|
||||
%if &mdebug ne 1 %then %do; /* have already output above */
|
||||
data _null_;infile &fname1;input;putlog _infile_;run;
|
||||
%end;
|
||||
%mp_abort(mac=&sysmacroname
|
||||
,msg=%str(logfetch: &SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE)
|
||||
)
|
||||
%end;
|
||||
|
||||
data _null_;
|
||||
file "&fpath3..lua";
|
||||
put '
|
||||
|
||||
@@ -97,8 +97,8 @@
|
||||
run;
|
||||
|
||||
|
||||
@param [in] access_token_var= The global macro variable to contain the access
|
||||
token
|
||||
@param [in] access_token_var= The global macro variable to contain the
|
||||
access token
|
||||
@param [in] grant_type= valid values:
|
||||
@li password
|
||||
@li authorization_code
|
||||
@@ -237,123 +237,143 @@ data;run;%let jdswaitfor=&syslast;
|
||||
|
||||
/* start loop */
|
||||
%do fid=1 %to &flowcnt;
|
||||
%put preparing job attributes for flow &&flow&fid;
|
||||
%local jds jcnt;
|
||||
data &jds(drop=_contextName _program);
|
||||
set &inds(where=(flow_id=&&flow&fid));
|
||||
if _contextName='' then _contextName="SAS Job Execution compute context";
|
||||
call symputx(cats('job',_n_),_program,'l');
|
||||
call symputx(cats('context',_n_),_contextName,'l');
|
||||
call symputx('jcnt',_n_,'l');
|
||||
run;
|
||||
%put exporting job variables in json format;
|
||||
%do jid=1 %to &jcnt;
|
||||
data &jjson;
|
||||
set &jds;
|
||||
if _n_=&jid then do;
|
||||
output;
|
||||
stop;
|
||||
end;
|
||||
run;
|
||||
proc json out=&jfref;
|
||||
export &jjson / nosastags fmtnumeric;
|
||||
run;
|
||||
data _null_;
|
||||
infile &jfref lrecl=32767;
|
||||
input;
|
||||
jparams='jparams'!!left(symget('jid'));
|
||||
call symputx(jparams,substr(_infile_,3,length(_infile_)-4));
|
||||
run;
|
||||
%local jobuid&jid;
|
||||
%let jobuid&jid=0; /* used in next loop */
|
||||
%end;
|
||||
%local concurrency completed;
|
||||
%let concurrency=0;
|
||||
%let completed=0;
|
||||
proc sql; drop table &jdsrunning;
|
||||
%do jid=1 %to &jcnt;
|
||||
/**
|
||||
* now we can execute the jobs up to the maxconcurrency setting
|
||||
*/
|
||||
%if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
|
||||
|
||||
/* check to see if the job finished in the previous round */
|
||||
%if %sysfunc(exist(&outds))=1 %then %do;
|
||||
%local jobcheck; %let jobcheck=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: jobcheck
|
||||
from &outds where uuid="&&jobuid&jid";
|
||||
%if &jobcheck>0 %then %do;
|
||||
%put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
|
||||
%let job&jid=0;
|
||||
%if not ( &raise_err and &syscc ) %then %do;
|
||||
|
||||
%put preparing job attributes for flow &&flow&fid;
|
||||
%local jds jcnt;
|
||||
data &jds(drop=_contextName _program);
|
||||
set &inds(where=(flow_id=&&flow&fid));
|
||||
if _contextName='' then _contextName="SAS Job Execution compute context";
|
||||
call symputx(cats('job',_n_),_program,'l');
|
||||
call symputx(cats('context',_n_),_contextName,'l');
|
||||
call symputx('jcnt',_n_,'l');
|
||||
run;
|
||||
%put exporting job variables in json format;
|
||||
%do jid=1 %to &jcnt;
|
||||
data &jjson;
|
||||
set &jds;
|
||||
if _n_=&jid then do;
|
||||
output;
|
||||
stop;
|
||||
end;
|
||||
run;
|
||||
proc json out=&jfref;
|
||||
export &jjson / nosastags fmtnumeric;
|
||||
run;
|
||||
data _null_;
|
||||
infile &jfref lrecl=32767;
|
||||
input;
|
||||
jparams='jparams'!!left(symget('jid'));
|
||||
call symputx(jparams,substr(_infile_,3,length(_infile_)-4));
|
||||
run;
|
||||
%local jobuid&jid;
|
||||
%let jobuid&jid=0; /* used in next loop */
|
||||
%end;
|
||||
%local concurrency completed;
|
||||
%let concurrency=0;
|
||||
%let completed=0;
|
||||
proc sql; drop table &jdsrunning;
|
||||
%do jid=1 %to &jcnt;
|
||||
/**
|
||||
* now we can execute the jobs up to the maxconcurrency setting
|
||||
*/
|
||||
%if "&&job&jid" ne "0" %then %do; /* this var is zero if job finished */
|
||||
|
||||
/* check to see if the job finished in the previous round */
|
||||
%if %sysfunc(exist(&outds))=1 %then %do;
|
||||
%local jobcheck; %let jobcheck=0;
|
||||
proc sql noprint;
|
||||
select count(*) into: jobcheck
|
||||
from &outds where uuid="&&jobuid&jid";
|
||||
%if &jobcheck>0 %then %do;
|
||||
%put &&job&jid in flow &fid with uid &&jobuid&jid completed!;
|
||||
%let job&jid=0;
|
||||
%end;
|
||||
%end;
|
||||
|
||||
/* check if job was triggered and, if
|
||||
so, if we have enough slots to run? */
|
||||
%if ("&&jobuid&jid"="0") and (&concurrency<&maxconcurrency) %then %do;
|
||||
|
||||
/* But only start if no issues detected so far */
|
||||
%if not ( &raise_err and &syscc ) %then %do;
|
||||
|
||||
%local jobname jobpath;
|
||||
%let jobname=%scan(&&job&jid,-1,/);
|
||||
%let jobpath=
|
||||
%substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
|
||||
|
||||
%put executing &jobpath/&jobname with paramstring &&jparams&jid;
|
||||
%mv_jobexecute(path=&jobpath
|
||||
,name=&jobname
|
||||
,paramstring=%superq(jparams&jid)
|
||||
,outds=&jdsapp
|
||||
)
|
||||
data &jdsapp;
|
||||
format jobparams $32767.;
|
||||
set &jdsapp(where=(method='GET' and rel='state'));
|
||||
jobparams=symget("jparams&jid");
|
||||
/* uri here has the /state suffix */
|
||||
uuid=scan(uri,-2,'/');
|
||||
call symputx("jobuid&jid",uuid,'l');
|
||||
run;
|
||||
proc append base=&jdsrunning data=&jdsapp;
|
||||
run;
|
||||
%let concurrency=%eval(&concurrency+1);
|
||||
/* sleep one second after every request to smooth the impact */
|
||||
data _null_;
|
||||
call sleep(1,1);
|
||||
run;
|
||||
|
||||
%end;
|
||||
%else %do; /* Job was skipped due to problems */
|
||||
|
||||
%put jobid &&job&jid in flow &fid skipped due to SYSCC (&syscc);
|
||||
%let completed = %eval(&completed+1);
|
||||
%let job&jid=0; /* Indicate job has finished */
|
||||
|
||||
%end;
|
||||
|
||||
%end;
|
||||
%end;
|
||||
%if &jid=&jcnt %then %do;
|
||||
/* we are at the end of the loop - check which jobs have finished */
|
||||
%mv_jobwaitfor(ANY,inds=&jdsrunning,outds=&jdswaitfor,outref=&outref
|
||||
,raise_err=&raise_err,mdebug=&mdebug)
|
||||
%local done;
|
||||
%let done=%mf_nobs(&jdswaitfor);
|
||||
%if &done>0 %then %do;
|
||||
%let completed=%eval(&completed+&done);
|
||||
%let concurrency=%eval(&concurrency-&done);
|
||||
data &jdsapp;
|
||||
set &jdswaitfor;
|
||||
flow_id=&&flow&fid;
|
||||
uuid=scan(uri,-1,'/');
|
||||
run;
|
||||
proc append base=&outds data=&jdsapp;
|
||||
run;
|
||||
%end;
|
||||
proc sql;
|
||||
delete from &jdsrunning
|
||||
where uuid in (select uuid from &outds
|
||||
where state in ('canceled','completed','failed')
|
||||
);
|
||||
|
||||
/* check if job was triggered and if so, if we have enough slots to run */
|
||||
%if "&&jobuid&jid"="0" and &concurrency<&maxconcurrency %then %do;
|
||||
%local jobname jobpath;
|
||||
%let jobname=%scan(&&job&jid,-1,/);
|
||||
%let jobpath=
|
||||
%substr(&&job&jid,1,%length(&&job&jid)-%length(&jobname)-1);
|
||||
%put executing &jobpath/&jobname with paramstring &&jparams&jid;
|
||||
%mv_jobexecute(path=&jobpath
|
||||
,name=&jobname
|
||||
,paramstring=%superq(jparams&jid)
|
||||
,outds=&jdsapp
|
||||
,mdebug=&mdebug
|
||||
)
|
||||
data &jdsapp;
|
||||
format jobparams $32767.;
|
||||
set &jdsapp(where=(method='GET' and rel='state'));
|
||||
jobparams=symget("jparams&jid");
|
||||
/* uri here has the /state suffix */
|
||||
uuid=scan(uri,-2,'/');
|
||||
call symputx("jobuid&jid",uuid,'l');
|
||||
run;
|
||||
proc append base=&jdsrunning data=&jdsapp;
|
||||
run;
|
||||
%let concurrency=%eval(&concurrency+1);
|
||||
/* sleep one second after every request to smooth the impact */
|
||||
data _null_;
|
||||
call sleep(1,1);
|
||||
run;
|
||||
/* loop again if jobs are left */
|
||||
%if &completed < &jcnt %then %do;
|
||||
%let jid=0;
|
||||
%put looping flow &fid again;
|
||||
%put &completed of &jcnt jobs completed, &concurrency jobs running;
|
||||
%end;
|
||||
%end;
|
||||
%end;
|
||||
%if &jid=&jcnt %then %do;
|
||||
/* we are at the end of the loop - time to see which jobs have finished */
|
||||
%mv_jobwaitfor(ANY
|
||||
,inds=&jdsrunning
|
||||
,outds=&jdswaitfor
|
||||
,outref=&outref
|
||||
,raise_err=&raise_err
|
||||
,mdebug=&mdebug
|
||||
)
|
||||
%local done;
|
||||
%let done=%mf_nobs(&jdswaitfor);
|
||||
%if &done>0 %then %do;
|
||||
%let completed=%eval(&completed+&done);
|
||||
%let concurrency=%eval(&concurrency-&done);
|
||||
data &jdsapp;
|
||||
set &jdswaitfor;
|
||||
flow_id=&&flow&fid;
|
||||
uuid=scan(uri,-1,'/');
|
||||
run;
|
||||
proc append base=&outds data=&jdsapp;
|
||||
run;
|
||||
%end;
|
||||
proc sql;
|
||||
delete from &jdsrunning
|
||||
where uuid in (select uuid from &outds
|
||||
where state in ('canceled','completed','failed')
|
||||
);
|
||||
|
||||
/* loop again if jobs are left */
|
||||
%if &completed < &jcnt %then %do;
|
||||
%let jid=0;
|
||||
%put looping flow &fid again - &completed of &jcnt jobs completed,
|
||||
&concurrency jobs running;
|
||||
%end;
|
||||
%end;
|
||||
%end;
|
||||
%else %do;
|
||||
|
||||
%put Flow &&flow&fid skipped due to SYSCC (&syscc);
|
||||
|
||||
%end;
|
||||
/* back up and execute the next flow */
|
||||
%end;
|
||||
|
||||
Reference in New Issue
Block a user