Macros for SAS Application Developers
https://github.com/sasjs/core
mp_lockanytable.sas
Go to the documentation of this file.
1 /**
2  @file
3  @brief Mechanism for locking tables to prevent parallel modifications
4  @details Uses a control table to enable ANY table to be locked for updates
5  (not just SAS datasets).
6  Only useful if every update uses the macro! Used heavily within
7  [Data Controller for SAS](https://datacontroller.io).
8 
9  @param [in] action The action to be performed. Valid values:
10  @li LOCK - Sets the lock flag, also confirms if a SAS lock is available
11  @li UNLOCK - Unlocks the table
12  @param [in] lib= (WORK) The libref of the table to lock. Should already be
13  assigned.
14  @param [in] ds= The dataset to lock
15  @param [in] ref= A meaningful reference to enable the lock to be traced. Max
16  length is 200 characters.
17  @param [out] ctl_ds= (0) The control table which controls the actual locking.
18  Should already be assigned and available. The definition is available by
19  running the mddl_dc_locktable.sas macro.
20 
21  @param [in] loops= (25) Number of times to check for a lock.
22  @param [in] loop_secs= (1) Seconds to wait between each lock attempt
23 
24  <h4> SAS Macros </h4>
25  @li mf_fmtdttm.sas
26  @li mp_abort.sas
27  @li mp_lockfilecheck.sas
28  @li mf_getuser.sas
29 
30  <h4> Related Macros </h4>
31  @li mp_lockanytable.test.sas
32 
33  @version 9.2
34 
35 **/
36 
37 %macro mp_lockanytable(
38  action
39  ,lib= WORK
40  ,ds=0
41  ,ref=
42  ,ctl_ds=0
43  ,loops=25
44  ,loop_secs=1
45  );
46 data _null_;
47  if _n_=1 then putlog "&sysmacroname entry vars:";
48  set sashelp.vmacro;
49  where scope="&sysmacroname";
50  put name '=' value;
51 run;
52 
53 %mp_abort(iftrue= ("&ds"="0" and &action ne MAKETABLE)
54  ,mac=&sysmacroname
55  ,msg=%str(dataset was not provided)
56 )
57 %mp_abort(iftrue= (&ctl_ds=0)
58  ,mac=&sysmacroname
59  ,msg=%str(Control dataset was not provided)
60 )
61 
62 /* set up lib & mac vars */
63 %let lib=%upcase(&lib);
64 %let ds=%upcase(&ds);
65 %let action=%upcase(&action);
66 %local user x trans msg abortme;
67 %let user=%mf_getuser();
68 %let abortme=0;
69 
70 %mp_abort(iftrue= (&action ne LOCK & &action ne UNLOCK & &action ne MAKETABLE)
71  ,mac=&sysmacroname
72  ,msg=%str(Invalid action (&action) provided)
73 )
74 
75 /* if an err condition exists, exit before we even begin */
76 %mp_abort(iftrue= (&syscc>0 and &action=LOCK)
77  ,mac=&sysmacroname
78  ,msg=%str(aborting due to syscc=&syscc on LOCK entry)
79 )
80 
81 /* do not bother locking work tables (else may affect all WORK libraries) */
82 %if (%upcase(&lib)=WORK or %str(&lib)=%str()) & &action ne MAKETABLE %then %do;
83  %put NOTE: WORK libraries will not be registered in the locking system.;
84  %return;
85 %end;
86 
87 /* do not proceed if no observations can be processed */
88 %mp_abort(iftrue= (%sysfunc(getoption(OBS))=0)
89  ,mac=&sysmacroname
90  ,msg=%str(cannot continue when options obs = 0)
91 )
92 
93 %if &ACTION=LOCK %then %do;
94 
95  /* abort if a SAS lock is already in place, or cannot be applied */
96  %mp_lockfilecheck(&lib..&ds)
97 
98  /* next, check there is a record for this table */
99  %local record_exists_check;
100  proc sql noprint;
101  select count(*) into: record_exists_check from &ctl_ds
102  where LOCK_LIB ="&lib" and LOCK_DS="&ds";
103  quit;
104  %if &syscc>0 %then %put syscc=&syscc sqlrc=&sqlrc;
105  %if &record_exists_check=0 %then %do;
106  data _null_;
107  putlog "&sysmacroname: adding record to lock table..";
108  run;
109 
110  data ;
111  if 0 then set &ctl_ds;
112  LOCK_LIB ="&lib";
113  LOCK_DS="&ds";
114  LOCK_STATUS_CD='LOCKED';
115  LOCK_START_DTTM="%sysfunc(datetime(),%mf_fmtdttm())"dt;
116  LOCK_USER_NM="&user";
117  LOCK_PID="&sysjobid";
118  LOCK_REF="&ref";
119  output;stop;
120  run;
121  %let trans=&syslast;
122  proc append base=&ctl_ds data=&trans;
123  run;
124  %end;
125  /* if record does exist, perform lock attempts */
126  %else %do x=1 %to &loops;
127  data _null_;
128  putlog "&sysmacroname: attempting lock (iteration &x) "@;
129  putlog "at %sysfunc(datetime(),datetime19.) ..";
130  run;
131 
132  proc sql;
133  update &ctl_ds
134  set LOCK_STATUS_CD='LOCKED'
135  , LOCK_START_DTTM="%sysfunc(datetime(),%mf_fmtdttm())"dt
136  , LOCK_USER_NM="&user"
137  , LOCK_PID="&sysjobid"
138  , LOCK_REF="&ref"
139  where LOCK_LIB ="&lib" and LOCK_DS="&ds";
140  quit;
141  /**
142  * NOTE - occasionally SQL server will return an err code (deadlocked
143  * transaction). If so, ignore it, keep calm, and carry on..
144  */
145  %if &syscc>0 %then %do;
146  data _null_;
147  putlog 'NOTE-' / 'NOTE-';
148  putlog "NOTE- &sysmacroname: Update failed. "@;
149  putlog "Resetting err conditions and re-attempting.";
150  putlog "NOTE- syscc=&syscc syserr=&syserr sqlrc=&sqlrc";
151  putlog 'NOTE-' / 'NOTE-';
152  run;
153  %let syscc=0;
154  %let sqlrc=0;
155  %end;
156 
157  /* now check if the record was successfully updated */
158  %local success_check;
159  proc sql noprint;
160  select count(*) into: success_check from &ctl_ds
161  where LOCK_LIB ="&lib" and LOCK_DS="&ds"
162  and LOCK_PID="&sysjobid" and LOCK_STATUS_CD='LOCKED';
163  quit;
164  %if &success_check=0 %then %do;
165  %if &x < &loops %then %do;
166  /* pause before next check */
167  data _null_;
168  putlog 'NOTE-' / 'NOTE-';
169  putlog "NOTE- &sysmacroname: table locked, waiting "@;
170  putlog "%sysfunc(sleep(&loop_secs)) seconds.. ";
171  putlog "NOTE- (iteration &x of &loops)";
172  putlog 'NOTE-' / 'NOTE-';
173  run;
174  %end;
175  %else %do;
176  %let msg=Unable to lock &lib..&ds via &ctl_ds after &loops attempts.\n
177  Please ask your administrator to investigate!;
178  %let abortme=1;
179  %end;
180  %end;
181  %else %do;
182  data _null_;
183  putlog 'NOTE-' / 'NOTE-';
184  putlog "NOTE- &sysmacroname: Table &lib..&ds locked at "@;
185  putlog " %sysfunc(datetime(),datetime19.) (iteration &x)"@;
186  putlog 'NOTE-' / 'NOTE-';
187  run;
188  %if &syscc>0 %then %do;
189  %put setting syscc(&syscc) back to 0;
190  %let syscc=0;
191  %end;
192  %let x=&loops; /* no more iterations needed */
193  %end;
194  %end;
195 %end;
196 %else %if &ACTION=UNLOCK %then %do;
197  %local status cnt;
198  %let cnt=0;
199  proc sql noprint;
200  select count(*) into: cnt from &ctl_ds where LOCK_LIB ="&lib" & LOCK_DS="&ds";
201  %if &cnt=0 %then %do;
202  %put %str(WAR)NING: &lib..&ds was not previously locked in &ctl_ds!;
203  %end;
204  %else %do;
205  select LOCK_STATUS_CD into: status from &ctl_ds
206  where LOCK_LIB ="&lib" and LOCK_DS="&ds";
207  quit;
208  %if &syscc>0 %then %put syscc=&syscc sqlrc=&sqlrc;
209  %if &status=LOCKED %then %do;
210  data _null_;
211  putlog "&sysmacroname: unlocking &lib..&ds:";
212  run;
213  proc sql;
214  update &ctl_ds
215  set LOCK_STATUS_CD='UNLOCKED'
216  , LOCK_END_DTTM="%sysfunc(datetime(),%mf_fmtdttm())"dt
217  , LOCK_USER_NM="&user"
218  , LOCK_PID="&sysjobid"
219  , LOCK_REF="&ref"
220  where LOCK_LIB ="&lib" and LOCK_DS="&ds";
221  quit;
222  %end;
223  %else %if &status=UNLOCKED %then %do;
224  %put %str(WAR)NING: &lib..&ds is already unlocked!;
225  %end;
226  %else %do;
227  %put NOTE: Unrecognised STATUS_CD (&status) in &ctl_ds;
228  %let abortme=1;
229  %end;
230  %end;
231 %end;
232 %else %do;
233  %let msg=lock_anytable given unsupported action (&action);
234  %let abortme=1;
235 %end;
236 
237 /* catch errs - mp_abort must be called outside of a logic block */
238 %mp_abort(iftrue=(&abortme=1),
239  msg=%superq(msg),
240  mac=&sysmacroname
241 )
242 
243 %exit_macro:
244 data _null_;
245  put "&sysmacroname: Exit vars: action=&action lib=&lib ds=&ds";
246  put " syscc=&syscc sqlrc=&sqlrc syserr=&syserr";
247 run;
248 %mend mp_lockanytable;
249 
250