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.sql.SQLException;
019import java.util.Iterator;
020import java.util.List;
021
022import javax.jcr.Node;
023import javax.jcr.RepositoryException;
024import javax.jcr.Session;
025import javax.jcr.SimpleCredentials;
026import javax.sql.DataSource;
027
028import org.apache.commons.io.FileUtils;
029import org.apache.jackrabbit.api.management.MarkEventListener;
030import org.apache.jackrabbit.core.RepositoryContext;
031import org.apache.jackrabbit.core.data.DataIdentifier;
032import org.apache.jackrabbit.core.data.DataStore;
033import org.apache.jackrabbit.core.data.DataStoreException;
034import org.apache.jackrabbit.core.data.FileDataStore;
035import org.apache.jackrabbit.core.data.db.DbDataStore;
036import org.apache.jackrabbit.core.data.db.DerbyDataStore;
037import org.apache.jackrabbit.core.gc.GarbageCollector;
038import org.apache.jackrabbit.core.persistence.IterablePersistenceManager;
039import org.apache.jackrabbit.core.util.db.ConnectionFactory;
040import org.apache.jackrabbit.core.util.db.ConnectionHelper;
041import org.slf4j.Logger;
042import org.slf4j.LoggerFactory;
043
044/**
045 * DataStoreGarbageCollectorTask
046 */
047public class DataStoreGarbageCollectorTask extends AbstractMaintenanceTask implements MarkEventListener
048{
049    private static final int SYSTEM_GC_CALLS = 3;
050
051    /** The JCR Session bound to this task. */
052    protected Session _session;
053
054    private GarbageCollector _garbageCollector;
055    private List<IterablePersistenceManager> _pmList;
056
057    /** Internal counter for scanned node by the GC */
058    private int _scannedNodesCount;
059
060    @Override
061    protected void initialize() throws RepositoryException
062    {
063        // Create the repository and log in the session.
064        RepositoryContext repositoryContext = createRepositoryContext();
065        _session = repositoryContext.getRepository().login(new SimpleCredentials("__MAINTENANCE_TASK__", "".toCharArray()));
066
067        // Create the GC Object
068        this._garbageCollector = repositoryContext.getRepository().createDataStoreGarbageCollector();
069
070        _pmList = getAllPersistenceManager(repositoryContext);
071
072        // Initialize the task progress object.
073        int count = 0; // number of item that will be scanned.
074
075        try
076        {
077            for (IterablePersistenceManager pm : _pmList)
078            {
079                count += pm.getAllNodeIds(null, 0).size();
080            }
081
082            // When scan is finished, progress is set at 70%.
083            float total = (10f / 7) * count;
084
085            _progress = new TaskProgress(Math.max(total, 1));
086        }
087        catch (Exception e)
088        {
089            _progress = new TaskProgress(0);
090            _progress.setInErrorState(e);
091            _logger.error(e.getLocalizedMessage(), e);
092        }
093    }
094    
095    @Override
096    protected void setLogger()
097    {
098        setLogger(LoggerFactory.getLogger(DataStoreGarbageCollectorTask.class));
099    }
100
101    @Override
102    protected void apply() throws RepositoryException
103    {
104        // call System.gc() a few times before running the data store garbage collection. 
105        // Please note System.gc() does not guarantee all objects are garbage collected.
106        for (int i = 0; i < SYSTEM_GC_CALLS; i++)
107        {
108            System.gc();
109        }
110
111        // Log some information about the ds.
112        long startSize = _reportDataStoreInfo(_garbageCollector.getDataStore());
113
114        // Sleep
115        if (_garbageCollector.getDataStore() instanceof FileDataStore)
116        {
117            // make sure the file is old (access time resolution is 2 seconds)
118            try
119            {
120                Thread.sleep(2000);
121            }
122            catch (InterruptedException e)
123            {
124                _logger.error(e.getLocalizedMessage(), e);
125                throw new RuntimeException(e); 
126            }
127        }
128
129        // GC Management
130        _garbageCollector.setMarkEventListener(this);
131        _garbageCollector.setPersistenceManagerScan(true);
132        // gc.setSleepBetweenNodes(0);
133
134        // Run GC
135        try
136        {
137            _scannedNodesCount = 0;
138            _logger.info("Scanning the repository nodes...");
139            _garbageCollector.mark();
140            _logger.info(_scannedNodesCount + " nodes scanned.");
141
142            _logger.info("Deleting unused items... Please be patient.");
143            int deleted = _garbageCollector.sweep();
144            _logger.info(deleted + " unused items deleted.");
145            if (_progress != null)
146            {
147                _progress.progressRelativePercentage(50);
148            }
149
150            _logger.info("Finalizing the process...");
151
152            // Compressing derby DATASTORE table.
153            if (_garbageCollector.getDataStore() instanceof DerbyDataStore)
154            {
155                _logger.info("Reclaiming unused space, this may take several minutes depending on the size of the data store.");
156                _logger.info("Please be patient.");
157
158                DbDataStore ds = (DbDataStore) _garbageCollector.getDataStore();
159                DataSource dataSource = _getDataSource(ds);
160                String table = ds.getTablePrefix() + ds.getSchemaObjectPrefix() + "DATASTORE";
161                derbyCompressTable(dataSource, table, _logger);
162            }
163        }
164        finally
165        {
166            _garbageCollector.close();
167        }
168
169        // Log some information about the ds.
170        long finalSize = _reportDataStoreInfo(_garbageCollector.getDataStore());
171        long freedSize = startSize - finalSize;
172        _logger.info("Size of cleared data: " + FileUtils.byteCountToDisplaySize(freedSize) + "Ko");
173        _logger.info("The total released space on your disk can be different depending on the type of the data store used by your repository.");
174    }
175
176    @Override
177    protected void close()
178    {
179        if (_session != null)
180        {
181            _session.logout();
182        }
183
184        super.close();
185
186        if (_progress != null)
187        {
188            _progress.progressRelativePercentage(100);
189        }
190    }
191
192    private long _reportDataStoreInfo(DataStore ds) throws DataStoreException
193    {
194        long count = 0;
195        long total = 0;
196        Iterator<DataIdentifier> it = ds.getAllIdentifiers();
197        while (it.hasNext())
198        {
199            count++;
200            DataIdentifier id = it.next();
201            total += ds.getRecord(id).getLength();
202        }
203
204        StringBuilder sb = new StringBuilder();
205        sb.append("Datastore item count: ").append(count).append(" ");
206        sb.append("[total size: ").append(FileUtils.byteCountToDisplaySize(total)).append("]");
207        _logger.info(sb.toString());
208
209        return total;
210    }
211
212    /**
213     * Retrieve the data source of a data store.
214     * @param ds the data store
215     * @return the data source
216     * @throws RepositoryException when an error occurred
217     */
218    protected DataSource _getDataSource(DbDataStore ds) throws RepositoryException
219    {
220        DataSource dataSource = null;
221
222        try
223        {
224            ConnectionFactory cf = getRepositoryConfig().getConnectionFactory();
225            if (ds.getDataSourceName() == null || "".equals(ds.getDataSourceName()))
226            {
227                dataSource = cf.getDataSource(ds.getDriver(), ds.getUrl(), ds.getUser(), ds.getPassword());
228            }
229            else
230            {
231                dataSource = cf.getDataSource(ds.getDataSourceName());
232            }
233        }
234        catch (SQLException e)
235        {
236            _logger.error(e.getLocalizedMessage(), e);
237            throw new RuntimeException(e);
238        }
239        catch (RepositoryException e)
240        {
241            _logger.warn("Failed to retrieve the data source from data store " + ds.getUrl(), e);
242            throw e;
243        }
244        return dataSource;
245    }
246    
247    /**
248     * Reclaiming unused space. This is derby specific.
249     * By default, Derby does not return unused space to the operating system
250     * when updating or deleting data.
251     * @param dataSource the data source to compress
252     * @param table the table to compress
253     * @param logger the logger to use for error
254     */
255    public static void derbyCompressTable(DataSource dataSource, String table, Logger logger)
256    {
257        ConnectionHelper conHelper = new ConnectionHelper(dataSource, false);
258        String sql = "CALL SYSCS_UTIL.SYSCS_COMPRESS_TABLE(CURRENT SCHEMA, ?, 0)";
259
260        try
261        {
262            conHelper.query(sql, table);
263        }
264        catch (SQLException e)
265        {
266            logger.error(e.getLocalizedMessage(), e);
267            throw new RuntimeException(e);
268        }
269    }
270    
271    @Override
272    public void beforeScanning(Node n) throws RepositoryException
273    {
274        _scannedNodesCount++;
275        if (_progress != null)
276        {
277            _progress.progress();
278        }
279    }
280}