| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * |
| */ |
| package jdbm.recman; |
| |
| import java.io.IOException; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| |
| import jdbm.ActionRecordManager; |
| import jdbm.RecordManager; |
| import jdbm.helper.ActionContext; |
| import jdbm.helper.ActionVersioning; |
| import jdbm.helper.CacheEvictionException; |
| import jdbm.helper.DefaultSerializer; |
| import jdbm.helper.EntryIO; |
| import jdbm.helper.LRUCache; |
| import jdbm.helper.Serializer; |
| |
| import org.apache.directory.server.i18n.I18n; |
| |
| |
| /** |
| * |
| * TODO SnapshotRecordManager. |
| * |
| * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> |
| */ |
| public class SnapshotRecordManager implements ActionRecordManager |
| { |
| /** Wrapped RecordManager */ |
| protected RecordManager recordManager; |
| |
| |
| /** Per thread action context */ |
| private static final ThreadLocal < ActionContext > actionContextVar = |
| new ThreadLocal < ActionContext > () { |
| @Override |
| protected ActionContext initialValue() |
| { |
| return null; |
| } |
| }; |
| |
| /** Used for keeping track of actions versions */ |
| ActionVersioning versioning = new ActionVersioning(); |
| |
| /** Versioned cache */ |
| LRUCache<Long, Object> versionedCache; |
| |
| /** Passed to cache as IO callback */ |
| RecordIO recordIO = new RecordIO(); |
| |
| /** Lock used to serialize write actions and some management operatins */ |
| Lock bigLock = new ReentrantLock(); |
| |
| /** |
| * Construct a SanshotRecordManager wrapping another RecordManager |
| * |
| * @param recordManager Wrapped RecordManager |
| */ |
| public SnapshotRecordManager( RecordManager recordManager, int size) |
| { |
| if ( recordManager == null ) |
| { |
| throw new IllegalArgumentException( I18n.err( I18n.ERR_517 ) ); |
| } |
| |
| this.recordManager = recordManager; |
| |
| versionedCache = new LRUCache<Long ,Object>(recordIO, size); |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public ActionContext beginAction( boolean readOnly , String whoStarted ) |
| { |
| ActionContext actionContext = new ActionContext(); |
| ActionVersioning.Version version; |
| |
| if ( readOnly ) |
| { |
| version = versioning.beginReadAction(); |
| } |
| else |
| { |
| bigLock.lock(); |
| version = versioning.beginWriteAction(); |
| } |
| |
| actionContext.beginAction( readOnly, version, whoStarted ); |
| this.setCurrentActionContext( actionContext ); |
| |
| return actionContext; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void setCurrentActionContext( ActionContext context ) |
| { |
| ActionContext actionContext = actionContextVar.get(); |
| assert( actionContext == null ) : "Action Context Not Null: " + actionContext.getWhoStarted(); |
| actionContextVar.set( context ); |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void unsetCurrentActionContext( ActionContext context ) |
| { |
| ActionContext actionContext = actionContextVar.get(); |
| assert( actionContext == context ); |
| actionContextVar.set( null ); |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void endAction( ActionContext actionContext ) |
| { |
| ActionVersioning.Version minVersion = null; |
| |
| if ( actionContext.isReadOnlyAction() ) |
| { |
| ActionVersioning.Version version = actionContext.getVersion(); |
| minVersion = versioning.endReadAction( version ); |
| actionContext.endAction(); |
| } |
| else if ( actionContext.isWriteAction() ) |
| { |
| minVersion = versioning.endWriteAction(); |
| actionContext.endAction(); |
| bigLock.unlock(); |
| } |
| else |
| { |
| assert( false ); |
| } |
| |
| this.unsetCurrentActionContext( actionContext ); |
| |
| if ( minVersion != null ) |
| { |
| versionedCache.advanceMinReadVersion( minVersion.getVersion() ); |
| } |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void abortAction( ActionContext actionContext ) |
| { |
| ActionVersioning.Version minVersion = null; |
| |
| if ( actionContext.isReadOnlyAction() ) |
| { |
| ActionVersioning.Version version = actionContext.getVersion(); |
| minVersion = versioning.endReadAction( version ); |
| actionContext.endAction(); |
| } |
| else if ( actionContext.isWriteAction() ) |
| { |
| /* |
| * Do not let versioning know that write action is complete, |
| * so that the readers wont see the effect of the aborted |
| * txn. The sensible thing to do would be to have the underling |
| * record manager expose a abort action interface. When that lacks. |
| * the right thing for the upper layer to do would is to rollback whatever |
| * is part of what JDBM calls a txn. |
| */ |
| |
| actionContext.endAction(); |
| bigLock.unlock(); |
| } |
| else |
| { |
| assert( false ); |
| } |
| |
| this.unsetCurrentActionContext( actionContext ); |
| |
| if ( minVersion != null ) |
| { |
| versionedCache.advanceMinReadVersion( minVersion.getVersion() ); |
| } |
| } |
| |
| |
| /** |
| * Get the underlying Record Manager. |
| * |
| * @return underlying RecordManager |
| */ |
| public RecordManager getRecordManager() |
| { |
| return recordManager; |
| } |
| |
| |
| /** |
| * Inserts a new record using a custom serializer. |
| * |
| * @param obj the object for the new record. |
| * @return the rowid for the new record. |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public long insert( Object obj ) throws IOException |
| { |
| return insert( obj, DefaultSerializer.INSTANCE ); |
| } |
| |
| |
| /** |
| * Inserts a new record using a custom serializer. |
| * |
| * @param obj the object for the new record. |
| * @param serializer a custom serializer |
| * @return the rowid for the new record. |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public long insert( Object obj, Serializer serializer ) throws IOException |
| { |
| checkIfClosed(); |
| |
| ActionContext actionContext = actionContextVar.get(); |
| boolean startedAction = false; |
| boolean abortedAction = false; |
| |
| if ( actionContext == null ) |
| { |
| actionContext = this.beginAction( false, "insert missing action" ); |
| startedAction = true; |
| } |
| |
| long recid = 0; |
| |
| try |
| { |
| recid = recordManager.insert( obj, serializer ); |
| |
| versionedCache.put( new Long( recid ), obj, actionContext.getVersion().getVersion(), |
| serializer, false ); |
| } |
| catch ( IOException e ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw e; |
| } |
| catch ( CacheEvictionException except ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw new IOException( except.getLocalizedMessage() ); |
| } |
| finally |
| { |
| if ( startedAction && !abortedAction ) |
| { |
| this.endAction ( actionContext ); |
| } |
| } |
| |
| return recid; |
| } |
| |
| |
| /** |
| * Deletes a record. |
| * |
| * @param recid the rowid for the record that should be deleted. |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public void delete( long recid ) throws IOException |
| { |
| checkIfClosed(); |
| |
| ActionContext actionContext = actionContextVar.get(); |
| boolean startedAction = false; |
| boolean abortedAction = false; |
| |
| if ( actionContext == null ) |
| { |
| actionContext = this.beginAction( false, "delete missing action" ); |
| startedAction = true; |
| } |
| |
| // Update the cache |
| try |
| { |
| versionedCache.put( new Long( recid ), null, actionContext.getVersion().getVersion(), |
| null, false ); |
| } |
| catch ( IOException e ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw e; |
| } |
| catch ( CacheEvictionException except ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw new IOException( except.getLocalizedMessage() ); |
| } |
| finally |
| { |
| if ( startedAction && !abortedAction ) |
| { |
| this.endAction ( actionContext ); |
| } |
| } |
| } |
| |
| |
| /** |
| * Updates a record using standard Java serialization. |
| * |
| * @param recid the recid for the record that is to be updated. |
| * @param obj the new object for the record. |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public void update( long recid, Object obj ) throws IOException |
| { |
| update( recid, obj, DefaultSerializer.INSTANCE ); |
| } |
| |
| |
| /** |
| * Updates a record using a custom serializer. |
| * |
| * @param recid the recid for the record that is to be updated. |
| * @param obj the new object for the record. |
| * @param serializer a custom serializer |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public void update( long recid, Object obj, Serializer serializer ) throws IOException |
| { |
| checkIfClosed(); |
| ActionContext actionContext = actionContextVar.get(); |
| boolean startedAction = false; |
| boolean abortedAction = false; |
| |
| if ( actionContext == null ) |
| { |
| actionContext = this.beginAction( false, "update missing action" ); |
| startedAction = true; |
| } |
| |
| try |
| { |
| versionedCache.put( new Long( recid ), obj, actionContext.getVersion().getVersion(), |
| serializer, recid < 0 ); |
| } |
| catch ( IOException e ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw e; |
| } |
| catch ( CacheEvictionException except ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw new IOException( except.getLocalizedMessage() ); |
| } |
| finally |
| { |
| if ( startedAction && !abortedAction ) |
| { |
| this.endAction ( actionContext ); |
| } |
| } |
| } |
| |
| |
| /** |
| * Fetches a record using standard Java serialization. |
| * |
| * @param recid the recid for the record that must be fetched. |
| * @return the object contained in the record. |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public Object fetch( long recid ) throws IOException |
| { |
| return fetch( recid, DefaultSerializer.INSTANCE ); |
| } |
| |
| |
| /** |
| * Fetches a record using a custom serializer. |
| * |
| * @param recid the recid for the record that must be fetched. |
| * @param serializer a custom serializer |
| * @return the object contained in the record. |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public Object fetch( long recid, Serializer serializer ) throws IOException |
| { |
| checkIfClosed(); |
| Object obj; |
| ActionContext actionContext = actionContextVar.get(); |
| |
| boolean startedAction = false; |
| boolean abortedAction = false; |
| |
| if ( actionContext == null ) |
| { |
| actionContext = this.beginAction( false, "fetch missing action" ); |
| startedAction = true; |
| } |
| |
| try |
| { |
| obj = versionedCache.get( new Long( recid ), actionContext.getVersion().getVersion(), |
| serializer ); |
| } |
| catch ( IOException e ) |
| { |
| if ( startedAction ) |
| { |
| this.abortAction( actionContext ); |
| abortedAction = true; |
| } |
| |
| throw e; |
| } |
| finally |
| { |
| if ( startedAction && !abortedAction ) |
| { |
| this.endAction ( actionContext ); |
| } |
| } |
| |
| return obj; |
| } |
| |
| |
| /** |
| * Closes the record manager. |
| * |
| * @throws IOException when one of the underlying I/O operations fails. |
| */ |
| public void close() throws IOException |
| { |
| checkIfClosed(); |
| |
| // Maybe quiesce all actions ..( not really required) |
| recordManager.close(); |
| recordManager = null; |
| versionedCache = null; |
| versioning = null; |
| } |
| |
| |
| /** |
| * Returns the number of slots available for "root" rowids. These slots |
| * can be used to store special rowids, like rowids that point to |
| * other rowids. Root rowids are useful for bootstrapping access to |
| * a set of data. |
| */ |
| public int getRootCount() |
| { |
| checkIfClosed(); |
| |
| return recordManager.getRootCount(); |
| } |
| |
| |
| /** |
| * Returns the indicated root rowid. |
| * |
| * @see #getRootCount |
| */ |
| public long getRoot( int id ) throws IOException |
| { |
| bigLock.lock(); |
| |
| try |
| { |
| checkIfClosed(); |
| return recordManager.getRoot( id ); |
| } |
| finally |
| { |
| bigLock.unlock(); |
| } |
| } |
| |
| |
| /** |
| * Sets the indicated root rowid. |
| * |
| * @see #getRootCount |
| */ |
| public void setRoot( int id, long rowid ) throws IOException |
| { |
| bigLock.lock(); |
| |
| try |
| { |
| checkIfClosed(); |
| |
| recordManager.setRoot( id, rowid ); |
| } |
| finally |
| { |
| bigLock.unlock(); |
| } |
| } |
| |
| |
| /** |
| * Commit (make persistent) all changes since beginning of transaction. |
| */ |
| public void commit() throws IOException |
| { |
| bigLock.lock(); |
| |
| try |
| { |
| checkIfClosed(); |
| |
| recordManager.commit(); |
| } |
| finally |
| { |
| bigLock.unlock(); |
| } |
| } |
| |
| |
| /** |
| * Rollback (cancel) all changes since beginning of transaction. |
| */ |
| public void rollback() throws IOException |
| { |
| // TODO handle this by quiecesing all actions and throwing away the cache contents |
| } |
| |
| |
| /** |
| * Obtain the record id of a named object. Returns 0 if named object |
| * doesn't exist. |
| */ |
| public long getNamedObject( String name ) throws IOException |
| { |
| bigLock.lock(); |
| |
| try |
| { |
| checkIfClosed(); |
| |
| return recordManager.getNamedObject( name ); |
| } |
| finally |
| { |
| bigLock.unlock(); |
| } |
| } |
| |
| |
| /** |
| * Set the record id of a named object. |
| */ |
| public void setNamedObject( String name, long recid ) throws IOException |
| { |
| bigLock.lock(); |
| |
| try |
| { |
| checkIfClosed(); |
| |
| recordManager.setNamedObject( name, recid ); |
| } |
| finally |
| { |
| bigLock.unlock(); |
| } |
| } |
| |
| |
| /** |
| * Check if RecordManager has been closed. If so, throw an IllegalStateException |
| */ |
| private void checkIfClosed() throws IllegalStateException |
| { |
| if ( recordManager == null ) |
| { |
| throw new IllegalStateException( I18n.err( I18n.ERR_538 ) ); |
| } |
| } |
| |
| |
| private class RecordIO implements EntryIO<Long, Object> |
| { |
| public Object read( Long key, Serializer serializer) throws IOException |
| { |
| // Meta objects are kept in memory only |
| if ( key < 0 ) |
| { |
| return null; |
| } |
| |
| return recordManager.fetch( key.longValue(), serializer ); |
| } |
| |
| public void write( Long key, Object value, Serializer serializer ) throws IOException |
| { |
| if ( key < 0 ) |
| { |
| return; |
| } |
| |
| if ( value != null ) |
| { |
| recordManager.update( key.longValue(), value , serializer ); |
| } |
| else |
| { |
| recordManager.delete( key.longValue() ); |
| } |
| } |
| } |
| } |