/** * */ package vnet.sms.common.cachewriter.cassandra; import static org.apache.commons.lang.Validate.isTrue; import static org.apache.commons.lang.Validate.notNull; import java.lang.annotation.Annotation; import java.util.Collection; import net.sf.ehcache.CacheEntry; import net.sf.ehcache.CacheException; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.writer.CacheWriter; import net.sf.ehcache.writer.writebehind.operations.SingleOperationType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import vnet.sms.common.cachewriter.cassandra.internal.DefaultCacheWriterAnnotations; import com.netflix.astyanax.AstyanaxContext; import com.netflix.astyanax.Keyspace; import com.netflix.astyanax.mapping.Mapping; import com.netflix.astyanax.mapping.MappingCache; import com.netflix.astyanax.mapping.MappingUtil; import com.netflix.astyanax.model.ColumnFamily; import com.netflix.astyanax.serializers.ObjectSerializer; import com.netflix.astyanax.serializers.StringSerializer; /** * @author obergner * */ public class CassandraCacheWriter implements CacheWriter { private final Logger log = LoggerFactory .getLogger(getClass()); private final AstyanaxContext<Keyspace> context; private CacheWriterImpl delegate; /** * @param context */ CassandraCacheWriter(final AstyanaxContext<Keyspace> context) { notNull(context, "Argument 'context' must not be null"); this.context = context; } /** * @see net.sf.ehcache.writer.CacheWriter#clone(net.sf.ehcache.Ehcache) */ @Override public CacheWriter clone(final Ehcache cache) throws CloneNotSupportedException { throw new CloneNotSupportedException( "CassandraCacheWriter cannot be cloned"); } /** * @see net.sf.ehcache.writer.CacheWriter#init() */ @Override public void init() { if (this.delegate != null) { throw new IllegalStateException( "Illegal attempt to re-initialize already initialized CacheWriter " + this); } this.log.info("Initializing {}", this); this.context.start(); this.log.info("Started Astyanax Cassandra Context {}", this.context); this.delegate = new CacheWriterImpl(this.context.getEntity()); this.log.info("{} successfully initialized", this); } /** * @see net.sf.ehcache.writer.CacheWriter#dispose() */ @Override public void dispose() throws CacheException { this.log.info("Shutting down {} ...", this); this.context.shutdown(); this.log.info("{} shut down", this); } private CacheWriterImpl getMandatoryDelegate() { checkInitialized(); return this.delegate; } private final void checkInitialized() throws IllegalStateException { if (this.delegate == null) { throw new IllegalStateException( "CacheWriter " + this + " has not yet been initialized - did you remember to call init() before using this CacheWriter instance?"); } } /** * @see net.sf.ehcache.writer.CacheWriter#write(net.sf.ehcache.Element) */ @Override public void write(final Element element) throws CacheException { getMandatoryDelegate().write(element); } /** * @see net.sf.ehcache.writer.CacheWriter#writeAll(java.util.Collection) */ @Override public void writeAll(final Collection<Element> elements) throws CacheException { getMandatoryDelegate().writeAll(elements); } /** * @see net.sf.ehcache.writer.CacheWriter#delete(net.sf.ehcache.CacheEntry) */ @Override public void delete(final CacheEntry entry) throws CacheException { getMandatoryDelegate().delete(entry); } /** * @see net.sf.ehcache.writer.CacheWriter#deleteAll(java.util.Collection) */ @Override public void deleteAll(final Collection<CacheEntry> entries) throws CacheException { getMandatoryDelegate().deleteAll(entries); } /** * @see net.sf.ehcache.writer.CacheWriter#throwAway(net.sf.ehcache.Element, * net.sf.ehcache.writer.writebehind.operations.SingleOperationType, * java.lang.RuntimeException) */ @Override public void throwAway(final Element element, final SingleOperationType operationType, final RuntimeException e) { getMandatoryDelegate().throwAway(element, operationType, e); } @Override public String toString() { return "CassandraCacheWriter@" + this.hashCode() + "[context: " + this.context + "|delegate: " + this.delegate + "]"; } private static final class CacheWriterImpl implements CacheWriter { private final Logger log = LoggerFactory .getLogger(getClass()); private final Keyspace keyspace; private final MappingUtil mapper; private final CacheWriterAnnotations<? extends Annotation, ? extends Annotation, ?> mappingAnnotations = new DefaultCacheWriterAnnotations(); CacheWriterImpl(final Keyspace keyspace) { this.keyspace = keyspace; this.mapper = new MappingUtil(keyspace, new MappingCache(), this.mappingAnnotations); } @Override public CacheWriter clone(final Ehcache cache) throws CloneNotSupportedException { throw new CloneNotSupportedException( "CassandraCacheWriter cannot be cloned"); } @Override public void init() { // Noop } @Override public void dispose() throws CacheException { } @Override public void write(final Element element) throws CacheException { try { checkElement(element); this.log.debug( "Persisting {} to backend Cassandra Keyspace {} ...", new Object[] { element, this.keyspace }); final ColumnFamily<Object, String> cf = columnFamilyFor( element, this.mappingAnnotations); this.mapper.put(cf, element.getObjectValue()); this.log.debug( "Finished persisting {} to backend Cassandra Keyspace {}", new Object[] { element, this.keyspace }); } catch (final Exception e) { throw new CacheException("Failed to persist " + element + " into keyspace " + this.keyspace + ": " + e.getMessage(), e); } } private void checkElement(final Element element) { notNull(element, "Argument 'element' must not be null"); checkElement(element.getObjectKey(), element.getObjectValue()); } private <T> void checkElement(final Object key, final T value) throws IllegalArgumentException { notNull(key, "Argument 'id' must not be null"); notNull(value, "Argument 'value' must not be null"); isTrue(value .getClass() .isAnnotationPresent( vnet.sms.common.cachewriter.cassandra.ColumnFamily.class), "Value [" + value + "] is missing mandatory annotation [" + vnet.sms.common.cachewriter.cassandra.ColumnFamily.class .getName() + "]"); final Mapping<T> valueMapping = (Mapping<T>) Mapping.make( value.getClass(), this.mappingAnnotations); final Object valueKey = valueMapping .getIdValue(value, Object.class); isTrue(key.equals(valueKey), "The element id [" + key + "] does not match the value [" + valueKey + "] of the field annotated with @Id"); } private <ID extends Annotation, COLUMN extends Annotation, COLUMNFAMILY extends Annotation> ColumnFamily<Object, String> columnFamilyFor( final Element element, final CacheWriterAnnotations<ID, COLUMN, COLUMNFAMILY> annotations) { isTrue(element .getObjectValue() .getClass() .isAnnotationPresent( vnet.sms.common.cachewriter.cassandra.ColumnFamily.class), "Value [" + element.getObjectValue() + "] is missing mandatory annotation [" + vnet.sms.common.cachewriter.cassandra.ColumnFamily.class .getName() + "]"); final Object value = element.getObjectValue(); final COLUMNFAMILY columnFamilyAnnotation = value.getClass() .getAnnotation(annotations.getColumnFamilyAnnotation()); final String columnFamilyName = annotations.getColumnFamilyName( value.getClass(), columnFamilyAnnotation); return new ColumnFamily<Object, String>(columnFamilyName, ObjectSerializer.get(), StringSerializer.get()); } @Override public void writeAll(final Collection<Element> elements) throws CacheException { this.log.debug( "Persisting {} to backend Cassandra Keyspace {} ...", new Object[] { elements, this.keyspace }); for (final Element elm : elements) { write(elm); } this.log.debug( "Finished persisting {} to backend Cassandra Keyspace {}", new Object[] { elements, this.keyspace }); } @Override public void delete(final CacheEntry entry) throws CacheException { try { notNull(entry.getElement(), "Can only delete CacheEntry that contains an Element to delete. Got: " + entry); checkElement(entry.getElement()); this.log.debug( "Deleting {} from backend Cassandra Keyspace {} ...", new Object[] { entry, this.keyspace }); final Element elementToDelete = entry.getElement(); final ColumnFamily<Object, String> cf = columnFamilyFor( elementToDelete, this.mappingAnnotations); this.mapper.remove(cf, elementToDelete.getObjectValue()); this.log.debug( "Finished deleting {} from backend Cassandra Keyspace {}", new Object[] { entry, this.keyspace }); } catch (final Exception e) { throw new CacheException("Failed to delete " + entry + " from backend keyspace " + this.keyspace + ": " + e.getMessage(), e); } } @Override public void deleteAll(final Collection<CacheEntry> entries) throws CacheException { this.log.debug( "Deleting {} from backend Cassandra Keyspace {} ...", new Object[] { entries, this.keyspace }); for (final CacheEntry entry : entries) { delete(entry); } this.log.debug( "Finished deleting {} from backend Cassandra Keyspace {}", new Object[] { entries, this.keyspace }); } @Override public void throwAway(final Element element, final SingleOperationType operationType, final RuntimeException e) { this.log.error( "THROWING AWAY [" + element + "] after operation [" + operationType + "] repeatedly failed due to [" + e.getMessage() + "]", e); } @Override public String toString() { return "CacheWriterImpl@" + this.hashCode() + "[keyspace: " + this.keyspace + "]"; } } }