001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.page;
018
019import java.time.Duration;
020import java.time.Instant;
021import java.util.Iterator;
022import java.util.concurrent.ConcurrentHashMap;
023import java.util.concurrent.ConcurrentMap;
024import java.util.function.Supplier;
025
026import org.apache.wicket.Application;
027import org.apache.wicket.settings.ExceptionSettings;
028import org.apache.wicket.util.LazyInitializer;
029import org.apache.wicket.util.lang.Args;
030import org.apache.wicket.util.lang.Threads;
031import org.apache.wicket.util.time.Durations;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * Default {@link IPageLockManager} that holds a map of locks in the current session.
037 */
038public class DefaultPageLockManager implements IPageLockManager {
039
040        private static final long serialVersionUID = 1L;
041
042        private static final Logger logger = LoggerFactory.getLogger(DefaultPageLockManager.class);
043
044        /** map of which pages are owned by which threads */
045        private final LazyInitializer<ConcurrentMap<Integer, PageAccessSynchronizer.PageLock>> locks = new LazyInitializer<>()
046        {
047                private static final long serialVersionUID = 1L;
048
049                @Override
050                protected ConcurrentMap<Integer, PageAccessSynchronizer.PageLock> createInstance()
051                {
052                        return new ConcurrentHashMap<>();
053                }
054        };
055
056        /** timeout value for acquiring a page lock */
057        private final Duration timeout;
058
059        /**
060         * Constructor
061         *
062         * @param timeout
063         *            timeout value for acquiring a page lock
064         */
065        public DefaultPageLockManager(Duration timeout)
066        {
067                this.timeout = Args.notNull(timeout, "timeout");
068        }
069
070        private static long remaining(Instant start, Duration timeout)
071        {
072                Duration elapsedTime = Durations.elapsedSince(start);
073                return Math.max(0, timeout.minus(elapsedTime).toMillis());
074        }
075
076        /**
077         * @param pageId
078         *            the id of the page to be locked
079         * @return the duration for acquiring a page lock
080         */
081        public Duration getTimeout(int pageId)
082        {
083                return timeout;
084        }
085
086        @Override
087        public void lockPage(int pageId) throws CouldNotLockPageException
088        {
089                final Thread thread = Thread.currentThread();
090                final PageAccessSynchronizer.PageLock lock = new PageAccessSynchronizer.PageLock(pageId, thread);
091                final Instant start = Instant.now();
092
093                boolean locked = false;
094
095                final boolean isDebugEnabled = logger.isDebugEnabled();
096
097                PageAccessSynchronizer.PageLock previous = null;
098
099                Duration pageTimeout = getTimeout(pageId);
100
101                while (!locked && Durations.elapsedSince(start).compareTo(pageTimeout) < 0)
102                {
103                        if (isDebugEnabled)
104                        {
105                                logger.debug("'{}' attempting to acquire lock to page with id '{}'",
106                                                thread.getName(), pageId);
107                        }
108
109                        previous = locks.get().putIfAbsent(pageId, lock);
110
111                        if (previous == null || previous.getThread() == thread)
112                        {
113                                // first thread to acquire lock or lock is already owned by this thread
114                                locked = true;
115                        }
116                        else
117                        {
118                                // wait for a lock to become available
119                                long remaining = remaining(start, pageTimeout);
120                                if (remaining > 0)
121                                {
122                                        previous.waitForRelease(remaining, isDebugEnabled);
123                                }
124                        }
125                }
126
127                if (locked)
128                {
129                        if (isDebugEnabled)
130                        {
131                                logger.debug("{} acquired lock to page {}", thread.getName(), pageId);
132                        }
133                }
134                else
135                {
136                        if (logger.isWarnEnabled())
137                        {
138                                final String previousThreadName = previous != null ? previous.getThread().getName() : "N/A";
139                                logger.warn(
140                                                "Thread '{}' failed to acquire lock to page with id '{}', attempted for {} out of allowed {}." +
141                                                                " The thread that holds the lock has name '{}'.",
142                                                thread.getName(), pageId, Duration.between(start, Instant.now()), pageTimeout, previousThreadName);
143                                if (Application.exists())
144                                {
145                                        ExceptionSettings.ThreadDumpStrategy strategy = Application.get()
146                                                        .getExceptionSettings()
147                                                        .getThreadDumpStrategy();
148                                        switch (strategy)
149                                        {
150                                                case ALL_THREADS :
151                                                        Threads.dumpAllThreads(logger);
152                                                        break;
153                                                case THREAD_HOLDING_LOCK :
154                                                        final Thread previousThread = previous != null ? previous.getThread() : null;
155                                                        if (previousThread != null)
156                                                        {
157                                                                Threads.dumpSingleThread(logger, previousThread);
158                                                        }
159                                                        else
160                                                        {
161                                                                logger.warn("Cannot dump the stack of the previous thread because it is not available.");
162                                                        }
163                                                        break;
164                                                case NO_THREADS :
165                                                default :
166                                                        // do nothing
167                                        }
168                                }
169                        }
170                        throw new CouldNotLockPageException(pageId, thread.getName(), pageTimeout);
171                }
172        }
173
174        @Override
175        public void unlockAllPages()
176        {
177                internalUnlockPages(null);
178        }
179
180        @Override
181        public void unlockPage(int pageId)
182        {
183                internalUnlockPages(pageId);
184        }
185
186        private void internalUnlockPages(final Integer pageId)
187        {
188                final Thread thread = Thread.currentThread();
189                final Iterator<PageAccessSynchronizer.PageLock> pageLockIterator = this.locks.get().values().iterator();
190
191                final boolean isDebugEnabled = logger.isDebugEnabled();
192
193                while (pageLockIterator.hasNext())
194                {
195                        // remove all locks held by this thread if 'pageId' is not specified
196                        // otherwise just the lock for this 'pageId'
197                        final PageAccessSynchronizer.PageLock lock = pageLockIterator.next();
198                        if ((pageId == null || pageId == lock.getPageId()) && lock.getThread() == thread)
199                        {
200                                pageLockIterator.remove();
201                                if (isDebugEnabled)
202                                {
203                                        logger.debug("'{}' released lock to page with id '{}'", thread.getName(),
204                                                        lock.getPageId());
205                                }
206                                // if any locks were removed notify threads waiting for a lock
207                                lock.markReleased(isDebugEnabled);
208                                if (pageId != null)
209                                {
210                                        // unlock just the page with the specified id
211                                        break;
212                                }
213                        }
214                }
215        }
216
217        /*
218         * used by tests
219         */
220        Supplier<ConcurrentMap<Integer, PageAccessSynchronizer.PageLock>> getLocks()
221        {
222                return locks;
223        }
224}