/** * * Copyright (c) 2014, the Railo Company Ltd. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library. If not, see <http://www.gnu.org/licenses/>. * **/ package lucee.runtime.tag; import lucee.runtime.PageContext; import lucee.runtime.PageContextImpl; import lucee.runtime.config.ConfigWebImpl; import lucee.runtime.debug.ActiveLock; import lucee.runtime.exp.ApplicationException; import lucee.runtime.exp.LockException; import lucee.runtime.exp.PageException; import lucee.runtime.ext.tag.BodyTagTryCatchFinallyImpl; import lucee.runtime.lock.LockData; import lucee.runtime.lock.LockManager; import lucee.runtime.lock.LockManagerImpl; import lucee.runtime.lock.LockTimeoutException; import lucee.runtime.lock.LockTimeoutExceptionImpl; import lucee.runtime.op.Caster; import lucee.runtime.type.Struct; import lucee.runtime.type.StructImpl; import lucee.runtime.type.dt.TimeSpan; import lucee.runtime.type.scope.ApplicationImpl; import lucee.runtime.type.scope.RequestImpl; import lucee.runtime.type.scope.ServerImpl; import lucee.runtime.util.PageContextUtil; /** * Provides two types of locks to ensure the integrity of shared data: Exclusive lock and Read-only * lock. An exclusive lock single-threads access to the CFML constructs in its body. Single-threaded access * implies that the body of the tag can be executed by at most one request at a time. A request executing * inside a cflock tag has an "exclusive lock" on the tag. No other requests can start executing inside the * tag while a request has an exclusive lock. CFML issues exclusive locks on a first-come, first-served * basis. A read-only lock allows multiple requests to access the CFML constructs inside its body concurrently. * Therefore, read-only locks should be used only when the shared data is read only and not modified. If another * request already has an exclusive lock on the shared data, the request waits for the exclusive lock to be * released. * * * **/ public final class Lock extends BodyTagTryCatchFinallyImpl { private static final short SCOPE_NONE=0; private static final short SCOPE_SERVER=1; private static final short SCOPE_APPLICATION=2; private static final short SCOPE_SESSION=3; private static final short SCOPE_REQUEST=4; private String id="anonymous"; /** Specifies the maximum amount of time, in seconds, to wait to obtain a lock. If a lock can be ** obtained within the specified period, execution continues inside the body of the tag. Otherwise, the ** behavior depends on the value of the throwOnTimeout attribute. */ private int timeoutInMillis; /** readOnly or Exclusive. Specifies the type of lock: read-only or exclusive. Default is Exclusive. ** A read-only lock allows more than one request to read shared data. An exclusive lock allows only one ** request to read or write to shared data. */ private short type=LockManager.TYPE_EXCLUSIVE; /** Specifies the scope as one of the following: Application, Server, or Session. This attribute is mutually ** exclusive with the name attribute. */ private short scope=SCOPE_NONE; /** Yes or No. Specifies how timeout conditions are handled. If the value is Yes, an exception is ** generated to provide notification of the timeout. If the value is No, execution continues past the ** cfclock tag. Default is Yes. */ private boolean throwontimeout = true; /** Specifies the name of the lock. */ private String name; private LockManager manager; private LockData data=null; private long start; @Override public void release() { super.release(); type = LockManager.TYPE_EXCLUSIVE; scope = SCOPE_NONE; throwontimeout = true; name = null; manager=null; this.data=null; id="anonymous"; timeoutInMillis=0; } /** * @param id the id to set */ public void setId(String id) { this.id = id; } /** set the value timeout * Specifies the maximum amount of time, in seconds, to wait to obtain a lock. If a lock can be * obtained within the specified period, execution continues inside the body of the tag. Otherwise, the * behavior depends on the value of the throwOnTimeout attribute. * @param timeout value to set **/ public void setTimeout(Object oTimeout) throws PageException { if(oTimeout instanceof TimeSpan) this.timeoutInMillis=toInt(((TimeSpan)oTimeout).getMillis()); else this.timeoutInMillis = toInt(Caster.toDoubleValue(oTimeout)*1000D); //print.out(Caster.toString(timeoutInMillis)); } public void setTimeout(double timeout) { this.timeoutInMillis = toInt(timeout*1000D); } /** set the value type * readOnly or Exclusive. Specifies the type of lock: read-only or exclusive. Default is Exclusive. * A read-only lock allows more than one request to read shared data. An exclusive lock allows only one * request to read or write to shared data. * @param type value to set * @throws ApplicationException **/ public void setType(String type) throws ApplicationException { type=type.toLowerCase().trim(); if(type.equals("exclusive")) { this.type=LockManager.TYPE_EXCLUSIVE; } else if(type.startsWith("read")){ this.type=LockManager.TYPE_READONLY; } else throw new ApplicationException("invalid value ["+type+"] for attribute [type] from tag [lock]", "valid values are [exclusive,read-only]"); } /** set the value scope * Specifies the scope as one of the following: Application, Server, or Session. This attribute is mutually * exclusive with the name attribute. * @param scope value to set * @throws ApplicationException **/ public void setScope(String scope) throws ApplicationException { scope=scope.toLowerCase().trim(); if(scope.equals("server"))this.scope=SCOPE_SERVER; else if(scope.equals("application"))this.scope=SCOPE_APPLICATION; else if(scope.equals("session"))this.scope=SCOPE_SESSION; else if(scope.equals("request"))this.scope=SCOPE_REQUEST; else throw new ApplicationException("invalid value ["+scope+"] for attribute [scope] from tag [lock]", "valid values are [server,application,session]"); } /** set the value throwontimeout * Yes or No. Specifies how timeout conditions are handled. If the value is Yes, an exception is * generated to provide notification of the timeout. If the value is No, execution continues past the * cfclock tag. Default is Yes. * @param throwontimeout value to set **/ public void setThrowontimeout(boolean throwontimeout) { this.throwontimeout = throwontimeout; } /** set the value name * @param name value to set * @throws ApplicationException **/ public void setName(String name) throws ApplicationException { if(name==null) return; this.name = name.trim(); if(name.length()==0)throw new ApplicationException("invalid attribute definition","attribute [name] can't be a empty string"); } @Override public int doStartTag() throws PageException { if(timeoutInMillis<=0) { TimeSpan remaining = PageContextUtil.remainingTime(pageContext,true); this.timeoutInMillis=toInt(remaining.getMillis()); } manager=pageContext.getConfig().getLockManager(); // check attributes if(name!=null && scope!=SCOPE_NONE) { throw new LockException( LockException.OPERATION_CREATE, this.name, "invalid attribute combination", "attribute [name] and [scope] can't be used together"); } if(name==null && scope==SCOPE_NONE) { name="id-"+id; } String lockType = null; if(name==null) { String cid=pageContext.getConfig().getIdentification().getId(); // Session if(scope==SCOPE_REQUEST){ lockType="request"; name="__request_"+cid+"__"+ ((RequestImpl)pageContext.requestScope())._getId(); } // Session else if(scope==SCOPE_SESSION){ lockType="session"; name="__session_"+cid+"__"+ pageContext.sessionScope()._getId(); } // Application else if(scope==SCOPE_APPLICATION){ lockType="application"; name="__application_"+cid+"__"+((ApplicationImpl)pageContext.applicationScope())._getId(); } // Server else if(scope==SCOPE_SERVER){ lockType="server"; name="__server_"+((ServerImpl)pageContext.serverScope())._getId(); } } Struct cflock=new StructImpl(); cflock.set("succeeded",Boolean.TRUE); cflock.set("errortext",""); pageContext.variablesScope().set("cflock",cflock); start=System.nanoTime(); try { ((PageContextImpl)pageContext).setActiveLock(new ActiveLock(type,name,timeoutInMillis)); // this has to be first, otherwise LockTimeoutException has nothing to release data = manager.lock(type,name,timeoutInMillis,pageContext.getId()); } catch (LockTimeoutException e) { LockManagerImpl mi = (LockManagerImpl)manager; Boolean hasReadLock = mi.isReadLocked(name); Boolean hasWriteLock = mi.isWriteLocked(name); String msg = LockTimeoutExceptionImpl.createMessage(type, name, lockType, timeoutInMillis, hasReadLock, hasWriteLock); _release(pageContext,System.nanoTime()-start); name=null; cflock.set("succeeded",Boolean.FALSE); cflock.set("errortext",msg); if(throwontimeout) throw new LockException( LockException.OPERATION_TIMEOUT, this.name, msg); return SKIP_BODY; } catch (InterruptedException e) { _release(pageContext,System.nanoTime()-start); cflock.set("succeeded",Boolean.FALSE); cflock.set("errortext",e.getMessage()); if(throwontimeout) throw Caster.toPageException(e); return SKIP_BODY; } return EVAL_BODY_INCLUDE; } private int toInt(long l) { if(l>Integer.MAX_VALUE)return Integer.MAX_VALUE; return (int)l; } private int toInt(double d) { if(d>Integer.MAX_VALUE)return Integer.MAX_VALUE; return (int)d; } private void _release(PageContext pc, long exe) { ActiveLock al = ((PageContextImpl)pc).releaseActiveLock(); // listener ((ConfigWebImpl)pc.getConfig()).getActionMonitorCollector() .log(pageContext, "lock", "Lock", exe, al.name+":"+al.timeoutInMillis); } @Override public void doFinally() { _release(pageContext,System.nanoTime()-start); if(name!=null)manager.unlock(data); } }