001/**
002 *
003 * Copyright (c) 2014, the Railo Company Ltd. All rights reserved.
004 *
005 * This library is free software; you can redistribute it and/or
006 * modify it under the terms of the GNU Lesser General Public
007 * License as published by the Free Software Foundation; either 
008 * version 2.1 of the License, or (at your option) any later version.
009 * 
010 * This library is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * Lesser General Public License for more details.
014 * 
015 * You should have received a copy of the GNU Lesser General Public 
016 * License along with this library.  If not, see <http://www.gnu.org/licenses/>.
017 * 
018 **/
019package lucee.runtime.tag;
020
021import lucee.runtime.PageContext;
022import lucee.runtime.PageContextImpl;
023import lucee.runtime.config.ConfigWebImpl;
024import lucee.runtime.debug.ActiveLock;
025import lucee.runtime.exp.ApplicationException;
026import lucee.runtime.exp.LockException;
027import lucee.runtime.exp.PageException;
028import lucee.runtime.ext.tag.BodyTagTryCatchFinallyImpl;
029import lucee.runtime.lock.LockData;
030import lucee.runtime.lock.LockManager;
031import lucee.runtime.lock.LockManagerImpl;
032import lucee.runtime.lock.LockTimeoutException;
033import lucee.runtime.lock.LockTimeoutExceptionImpl;
034import lucee.runtime.op.Caster;
035import lucee.runtime.type.Struct;
036import lucee.runtime.type.StructImpl;
037import lucee.runtime.type.dt.TimeSpan;
038import lucee.runtime.type.scope.ApplicationImpl;
039import lucee.runtime.type.scope.RequestImpl;
040import lucee.runtime.type.scope.ServerImpl;
041
042/**
043* Provides two types of locks to ensure the integrity of shared data: Exclusive lock and Read-only 
044*   lock. An exclusive lock single-threads access to the CFML constructs in its body. Single-threaded access 
045*   implies that the body of the tag can be executed by at most one request at a time. A request executing 
046*   inside a cflock tag has an "exclusive lock" on the tag. No other requests can start executing inside the 
047*   tag while a request has an exclusive lock. CFML issues exclusive locks on a first-come, first-served 
048*   basis. A read-only lock allows multiple requests to access the CFML constructs inside its body concurrently. 
049*   Therefore, read-only locks should be used only when the shared data is read only and not modified. If another 
050*   request already has an exclusive lock on the shared data, the request waits for the exclusive lock to be 
051*   released.
052*
053*
054*
055**/
056public final class Lock extends BodyTagTryCatchFinallyImpl {
057
058
059
060        private static final short SCOPE_NONE=0;
061        private static final short SCOPE_SERVER=1;
062        private static final short SCOPE_APPLICATION=2;
063        private static final short SCOPE_SESSION=3;
064        private static final short SCOPE_REQUEST=4;
065        
066        private String id="anonymous";
067        
068        /** Specifies the maximum amount of time, in seconds, to wait to obtain a lock. If a lock can be 
069        **              obtained within the specified period, execution continues inside the body of the tag. Otherwise, the 
070        **              behavior depends on the value of the throwOnTimeout attribute. */
071        private int timeoutInMillis;
072        
073        /** readOnly or Exclusive. Specifies the type of lock: read-only or exclusive. Default is Exclusive. 
074        **      A read-only lock allows more than one request to read shared data. An exclusive lock allows only one 
075        **      request to read or write to shared data. */
076        private short type=LockManager.TYPE_EXCLUSIVE;
077        
078        /** Specifies the scope as one of the following: Application, Server, or Session. This attribute is mutually 
079        **      exclusive with the name attribute. */
080        private short scope=SCOPE_NONE;
081        
082        /** Yes or No. Specifies how timeout conditions are handled. If the value is Yes, an exception is 
083        **      generated to provide notification of the timeout. If the value is No, execution continues past the 
084        **    cfclock tag. Default is Yes. */
085        private boolean throwontimeout = true;
086        
087        /** Specifies the name of the lock.  */
088        private String name;
089        
090        private LockManager manager;
091    private LockData data=null;
092        private long start;
093
094        @Override
095        public void release() {
096                super.release();
097                type = LockManager.TYPE_EXCLUSIVE;
098                scope = SCOPE_NONE;
099                throwontimeout = true;
100                name = null;
101        manager=null;
102        this.data=null;
103        id="anonymous";
104        timeoutInMillis=0;
105        }
106
107
108        /**
109         * @param id the id to set
110         */
111        public void setId(String id) {
112                this.id = id;
113        }
114        
115        /** set the value timeout
116        *  Specifies the maximum amount of time, in seconds, to wait to obtain a lock. If a lock can be 
117        *               obtained within the specified period, execution continues inside the body of the tag. Otherwise, the 
118        *               behavior depends on the value of the throwOnTimeout attribute.
119        * @param timeout value to set
120        **/
121        public void setTimeout(Object oTimeout) throws PageException {
122                if(oTimeout instanceof TimeSpan)
123                        this.timeoutInMillis=(int)((TimeSpan)oTimeout).getMillis();
124                else
125                        this.timeoutInMillis = (int)(Caster.toDoubleValue(oTimeout)*1000D);
126                //print.out(Caster.toString(timeoutInMillis));
127        }
128        public void setTimeout(double timeout) {
129                this.timeoutInMillis = (int)(timeout*1000D);
130        }
131
132        /** set the value type
133        *  readOnly or Exclusive. Specifies the type of lock: read-only or exclusive. Default is Exclusive. 
134        *       A read-only lock allows more than one request to read shared data. An exclusive lock allows only one 
135        *       request to read or write to shared data.
136        * @param type value to set
137         * @throws ApplicationException
138        **/
139        public void setType(String type) throws ApplicationException {
140                type=type.toLowerCase().trim();
141                                
142                if(type.equals("exclusive")) {
143                    this.type=LockManager.TYPE_EXCLUSIVE;
144                }
145                else if(type.startsWith("read")){
146                    this.type=LockManager.TYPE_READONLY;
147                }
148                else 
149                        throw new ApplicationException("invalid value ["+type+"] for attribute [type] from tag [lock]",
150                                        "valid values are [exclusive,read-only]");
151        }
152
153        /** set the value scope
154        *  Specifies the scope as one of the following: Application, Server, or Session. This attribute is mutually 
155        *       exclusive with the name attribute.
156        * @param scope value to set
157         * @throws ApplicationException
158        **/
159        public void setScope(String scope) throws ApplicationException {
160                scope=scope.toLowerCase().trim();
161                
162                if(scope.equals("server"))this.scope=SCOPE_SERVER;
163                else if(scope.equals("application"))this.scope=SCOPE_APPLICATION;
164                else if(scope.equals("session"))this.scope=SCOPE_SESSION;
165                else if(scope.equals("request"))this.scope=SCOPE_REQUEST;
166                else 
167                        throw new ApplicationException("invalid value ["+scope+"] for attribute [scope] from tag [lock]",
168                                        "valid values are [server,application,session]");
169        }
170
171        /** set the value throwontimeout
172        *  Yes or No. Specifies how timeout conditions are handled. If the value is Yes, an exception is 
173        *       generated to provide notification of the timeout. If the value is No, execution continues past the 
174        *    cfclock tag. Default is Yes.
175        * @param throwontimeout value to set
176        **/
177        public void setThrowontimeout(boolean throwontimeout) {
178                this.throwontimeout = throwontimeout;
179        }
180
181        /** set the value name
182        * @param name value to set
183         * @throws ApplicationException
184        **/
185        public void setName(String name) throws ApplicationException {
186                if(name==null) return;
187                this.name = name.trim();
188                if(name.length()==0)throw new ApplicationException("invalid attribute definition","attribute [name] can't be a empty string");
189        }
190
191        @Override
192        public int doStartTag() throws PageException {
193                //if(timeoutInMillis==0)timeoutInMillis=30000;
194                //print.out("doStartTag");
195            manager=pageContext.getConfig().getLockManager();
196        // check attributes
197            if(name!=null && scope!=SCOPE_NONE) {
198                throw new LockException(
199                        LockException.OPERATION_CREATE,
200                        this.name,
201                        "invalid attribute combination",
202                                "attribute [name] and [scope] can't be used together");
203        }
204            if(name==null && scope==SCOPE_NONE)    {
205                name="id-"+id;
206        }
207        
208            String lockType = null;
209            if(name==null) {
210                String cid=pageContext.getConfig().getId();
211                // Session
212                if(scope==SCOPE_REQUEST){
213                    lockType="request"; 
214                    name="__request_"+cid+"__"+ ((RequestImpl)pageContext.requestScope())._getId();
215                }
216                // Session
217                else if(scope==SCOPE_SESSION){
218                    lockType="session"; 
219                    name="__session_"+cid+"__"+ pageContext.sessionScope()._getId();
220                }
221                // Application 
222                else if(scope==SCOPE_APPLICATION){
223                    lockType="application";
224                    name="__application_"+cid+"__"+((ApplicationImpl)pageContext.applicationScope())._getId();
225                }
226                // Server
227                else if(scope==SCOPE_SERVER){
228                    lockType="server";
229                    name="__server_"+((ServerImpl)pageContext.serverScope())._getId();
230                }
231            }
232            Struct cflock=new StructImpl();
233            cflock.set("succeeded",Boolean.TRUE);
234            cflock.set("errortext","");
235            pageContext.variablesScope().set("cflock",cflock);
236        start=System.nanoTime();
237        try {
238                    ((PageContextImpl)pageContext).setActiveLock(new ActiveLock(type,name,timeoutInMillis)); // this has to be first, otherwise LockTimeoutException has nothing to release
239                    data = manager.lock(type,name,timeoutInMillis,pageContext.getId());
240                } 
241                catch (LockTimeoutException e) {
242                        LockManagerImpl mi = (LockManagerImpl)manager;
243                    Boolean hasReadLock = mi.isReadLocked(name);
244                    Boolean hasWriteLock = mi.isWriteLocked(name);
245                    String msg = LockTimeoutExceptionImpl.createMessage(type, name, lockType, timeoutInMillis, hasReadLock, hasWriteLock);
246                    
247                    _release(pageContext,System.nanoTime()-start);
248                    name=null;
249                    
250                    cflock.set("succeeded",Boolean.FALSE);
251                    cflock.set("errortext",msg);
252
253                        if(throwontimeout) throw new LockException(
254                        LockException.OPERATION_TIMEOUT,
255                        this.name,
256                        msg);
257                        
258                        return SKIP_BODY;
259                } 
260                catch (InterruptedException e) {
261                        _release(pageContext,System.nanoTime()-start);
262                    cflock.set("succeeded",Boolean.FALSE);
263                    cflock.set("errortext",e.getMessage());
264                    
265                    if(throwontimeout) throw Caster.toPageException(e);
266                        
267                        return SKIP_BODY;
268                    
269                    
270                }
271                
272                return EVAL_BODY_INCLUDE;
273        }
274        
275        private void _release(PageContext pc, long exe) {
276                ActiveLock al = ((PageContextImpl)pc).releaseActiveLock();
277            // listener
278                ((ConfigWebImpl)pc.getConfig()).getActionMonitorCollector()
279                        .log(pageContext, "lock", "Lock", exe, al.name+":"+al.timeoutInMillis);
280                
281        }
282
283
284        @Override
285        public void doFinally() {
286                _release(pageContext,System.nanoTime()-start);
287            if(name!=null)manager.unlock(data);
288        }
289        
290        
291}