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}