001/* 002* Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013* See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.workspaces.repository.maintenance; 017 018import java.lang.reflect.InvocationTargetException; 019import java.lang.reflect.Method; 020import java.sql.SQLException; 021import java.util.ArrayList; 022import java.util.Iterator; 023 024import javax.jcr.Node; 025import javax.jcr.RepositoryException; 026import javax.jcr.Session; 027import javax.jcr.SimpleCredentials; 028import javax.sql.DataSource; 029 030import org.apache.jackrabbit.api.management.MarkEventListener; 031import org.apache.jackrabbit.core.RepositoryContext; 032import org.apache.jackrabbit.core.data.DataIdentifier; 033import org.apache.jackrabbit.core.data.DataStore; 034import org.apache.jackrabbit.core.data.DataStoreException; 035import org.apache.jackrabbit.core.data.FileDataStore; 036import org.apache.jackrabbit.core.data.db.DbDataStore; 037import org.apache.jackrabbit.core.data.db.DerbyDataStore; 038import org.apache.jackrabbit.core.gc.GarbageCollector; 039import org.apache.jackrabbit.core.persistence.IterablePersistenceManager; 040import org.apache.jackrabbit.core.persistence.PersistenceManager; 041import org.apache.jackrabbit.core.util.db.ConnectionFactory; 042import org.apache.jackrabbit.core.util.db.ConnectionHelper; 043import org.apache.jackrabbit.core.version.InternalVersionManagerImpl; 044import org.slf4j.LoggerFactory; 045 046/** 047 * DataStoreGarbageCollectorTask 048 */ 049public class DataStoreGarbageCollectorTask extends AbstractMaintenanceTask implements MarkEventListener 050{ 051 private static final int SYSTEM_GC_CALLS = 3; 052 053 /** The JackRabbit RepositoryImpl Context */ 054 protected RepositoryContext _repositoryContext; 055 056 /** The JCR Session bound to this task. */ 057 protected Session _session; 058 059 private GarbageCollector _garbageCollector; 060 private IterablePersistenceManager[] _pmList; 061 062 /** Internal counter for scanned node by the GC */ 063 private int _scannedNodesCount; 064 065 @Override 066 protected void initialize() throws RepositoryException 067 { 068 // Create the repository and log in the session. 069 _repositoryContext = RepositoryContext.create(_repositoryConfig); 070 _session = _repositoryContext.getRepository().login(new SimpleCredentials("__MAINTENANCE_TASK__", "".toCharArray())); 071 072 // Create the GC Object 073 this._garbageCollector = _repositoryContext.getRepository().createDataStoreGarbageCollector(); 074 075 // Workaround to get the list of the PersistenceManager 076 ArrayList<PersistenceManager> pmList = new ArrayList<>(); 077 InternalVersionManagerImpl vm = _repositoryContext.getInternalVersionManager(); 078 pmList.add(vm.getPersistenceManager()); 079 080 String[] wspNames = _repositoryContext.getWorkspaceManager().getWorkspaceNames(); 081 for (int i = 0; i < wspNames.length; i++) 082 { 083 pmList.add(getPM(wspNames[i])); 084 } 085 086 _pmList = new IterablePersistenceManager[pmList.size()]; 087 for (int i = 0; i < pmList.size(); i++) 088 { 089 PersistenceManager pm = pmList.get(i); 090 if (!(pm instanceof IterablePersistenceManager)) 091 { 092 _pmList = null; 093 break; 094 } 095 _pmList[i] = (IterablePersistenceManager) pm; 096 } 097 098 // Initialize the task progress object. 099 int count = 0; // number of item that will be scanned. 100 101 try 102 { 103 for (IterablePersistenceManager pm : _pmList) 104 { 105 count += pm.getAllNodeIds(null, 0).size(); 106 } 107 108 // When scan is finished, progress is set at 70%. 109 float total = (10f / 7) * count; 110 111 _progress = new TaskProgress(Math.max(total, 1)); 112 } 113 catch (Exception e) 114 { 115 _progress = new TaskProgress(0); 116 _progress.setInErrorState(e); 117 _logger.error(e.getLocalizedMessage(), e); 118 } 119 } 120 121 @Override 122 protected void setLogger() 123 { 124 setLogger(LoggerFactory.getLogger(DataStoreGarbageCollectorTask.class)); 125 } 126 127 @Override 128 protected void apply() throws RepositoryException 129 { 130 // call System.gc() a few times before running the data store garbage collection. 131 // Please note System.gc() does not guarantee all objects are garbage collected. 132 for (int i = 0; i < SYSTEM_GC_CALLS; i++) 133 { 134 System.gc(); 135 } 136 137 // Log some information about the ds. 138 long startSize = _reportDataStoreInfo(_garbageCollector.getDataStore()); 139 140 // Sleep 141 if (_garbageCollector.getDataStore() instanceof FileDataStore) 142 { 143 // make sure the file is old (access time resolution is 2 seconds) 144 try 145 { 146 Thread.sleep(2000); 147 } 148 catch (InterruptedException e) 149 { 150 _logger.error(e.getLocalizedMessage(), e); 151 throw new RuntimeException(e); 152 } 153 } 154 155 // GC Management 156 _garbageCollector.setMarkEventListener(this); 157 _garbageCollector.setPersistenceManagerScan(true); 158 // gc.setSleepBetweenNodes(0); 159 160 // Run GC 161 try 162 { 163 _scannedNodesCount = 0; 164 _logger.info("Scanning the repository nodes..."); 165 _garbageCollector.mark(); 166 _logger.info(_scannedNodesCount + " nodes scanned."); 167 168 _logger.info("Deleting unused items... Please be patient."); 169 int deleted = _garbageCollector.sweep(); 170 _logger.info(deleted + " unused items deleted."); 171 if (_progress != null) 172 { 173 _progress.progressRelativePercentage(50); 174 } 175 176 _logger.info("Finalizing the process..."); 177 178 // Compressing derby DATASTORE table. 179 if (_garbageCollector.getDataStore() instanceof DerbyDataStore) 180 { 181 _logger.info("Reclaiming unused space, this may take several minutes depending on the size of the data store."); 182 _logger.info("Please be patient."); 183 184 DbDataStore ds = (DbDataStore) _garbageCollector.getDataStore(); 185 _derbyCompressTable(ds); 186 } 187 } 188 finally 189 { 190 _garbageCollector.close(); 191 } 192 193 // Log some information about the ds. 194 long finalSize = _reportDataStoreInfo(_garbageCollector.getDataStore()); 195 long freedSize = startSize - finalSize; 196 _logger.info("Size of cleared data : " + Math.round(freedSize / 1024) + "Ko"); 197 _logger.info("The total released space on your disk can be different depending on the type of the data store used by your repository."); 198 } 199 200 @Override 201 protected void close() 202 { 203 if (_session != null) 204 { 205 _session.logout(); 206 } 207 208 if (_repositoryContext != null && _repositoryContext.getRepository() != null) 209 { 210 _repositoryContext.getRepository().shutdown(); 211 } 212 213 if (_progress != null) 214 { 215 _progress.progressRelativePercentage(100); 216 } 217 } 218 219 private long _reportDataStoreInfo(DataStore ds) throws DataStoreException 220 { 221 long count = 0; 222 long total = 0; 223 Iterator<DataIdentifier> it = ds.getAllIdentifiers(); 224 while (it.hasNext()) 225 { 226 count++; 227 DataIdentifier id = it.next(); 228 total += ds.getRecord(id).getLength(); 229 } 230 231 StringBuilder sb = new StringBuilder(); 232 sb.append("Datastore item count : ").append(count).append(" "); 233 sb.append("[total size : ").append(Math.round(total / 1024)).append("Ko]"); 234 _logger.info(sb.toString()); 235 236 return total; 237 } 238 239 /** 240 * Reclaiming unused space. This is derby specific. 241 * By default, Derby does not return unused space to the operating system 242 * when updating or deleting data. 243 * @param ds The datastore 244 * @throws RepositoryException If an error occurs with the repository 245 */ 246 private void _derbyCompressTable(DbDataStore ds) throws RepositoryException 247 { 248 DataSource dataSource = null; 249 250 try 251 { 252 ConnectionFactory cf = _repositoryConfig.getConnectionFactory(); 253 if (ds.getDataSourceName() == null || "".equals(ds.getDataSourceName())) 254 { 255 dataSource = cf.getDataSource(ds.getDriver(), ds.getUrl(), ds.getUser(), ds.getPassword()); 256 } 257 else 258 { 259 dataSource = cf.getDataSource(ds.getDataSourceName()); 260 } 261 } 262 catch (SQLException e) 263 { 264 _logger.error(e.getLocalizedMessage(), e); 265 throw new RuntimeException(e); 266 } 267 268 if (dataSource != null) 269 { 270 ConnectionHelper conHelper = new ConnectionHelper(dataSource, false); 271 String sql = "CALL SYSCS_UTIL.SYSCS_COMPRESS_TABLE(CURRENT SCHEMA, ?, 0)"; 272 String prefix = ds.getTablePrefix(); 273 String schemaObjPrefix = ds.getSchemaObjectPrefix(); 274 String table = prefix + schemaObjPrefix + "DATASTORE"; 275 276 try 277 { 278 conHelper.query(sql, table); 279 } 280 catch (SQLException e) 281 { 282 _logger.error(e.getLocalizedMessage(), e); 283 throw new RuntimeException(e); 284 } 285 } 286 else 287 { 288 _logger.error("Unable to compress the Derby datastore, unused space has not been freed up."); 289 } 290 291 } 292 293 @Override 294 public void beforeScanning(Node n) throws RepositoryException 295 { 296 _scannedNodesCount++; 297 if (_progress != null) 298 { 299 _progress.progress(); 300 } 301 } 302 303 /** 304 * Retrieves JackRabbit Persistence Manager for currently opened repository. This method uses 305 * Privileged access and will fail with security exception if used in environment with enabled security manager. 306 * @param workspaceName The workspace name 307 * @return Persistence manager used by repository. 308 * @throws RepositoryException If an error occurs while retrieving the persistence manager 309 */ 310 protected PersistenceManager getPM(String workspaceName) throws RepositoryException 311 { 312 try 313 { 314 Object workspaceInfo = findAndInvokeMethod(_repositoryContext.getRepository(), "getWorkspaceInfo", new Object[] {workspaceName}); 315 return (PersistenceManager) (findAndInvokeMethod(workspaceInfo, "getPersistenceManager", null)); 316 } 317 catch (Exception e) 318 { 319 throw new RepositoryException(e); 320 } 321 } 322 323 private static Object findAndInvokeMethod(Object obj, String name, Object[] parameters) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException 324 { 325 Method m = null; 326 Method[] ms = obj.getClass().getDeclaredMethods(); 327 for (int i = 0; i < ms.length; i++) 328 { 329 final Method x = ms[i]; 330 if (x.getName().equals(name)) 331 { 332 m = x; 333 m.setAccessible(true); 334 return m.invoke(obj, parameters); 335 } 336 } 337 338 return null; 339 } 340}