/* * 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 org.apache.usergrid.persistence.collection.serialization.impl; import java.nio.ByteBuffer; import java.util.UUID; import com.codahale.metrics.Histogram; import com.codahale.metrics.Timer; import org.apache.usergrid.persistence.collection.MvccEntity; import org.apache.usergrid.persistence.collection.exception.DataCorruptionException; import org.apache.usergrid.persistence.collection.exception.EntityTooLargeException; import org.apache.usergrid.persistence.collection.serialization.SerializationFig; import org.apache.usergrid.persistence.core.CassandraFig; import org.apache.usergrid.persistence.core.astyanax.FieldBuffer; import org.apache.usergrid.persistence.core.astyanax.FieldBufferBuilder; import org.apache.usergrid.persistence.core.astyanax.FieldBufferParser; import org.apache.usergrid.persistence.core.astyanax.FieldBufferSerializer; import org.apache.usergrid.persistence.core.astyanax.IdRowCompositeSerializer; import org.apache.usergrid.persistence.core.astyanax.MultiTenantColumnFamily; import org.apache.usergrid.persistence.core.astyanax.ScopedRowKey; import org.apache.usergrid.persistence.core.metrics.MetricsFactory; import org.apache.usergrid.persistence.model.entity.Entity; import org.apache.usergrid.persistence.model.entity.Id; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.google.common.base.Optional; import com.google.inject.Inject; import com.google.inject.Singleton; import com.netflix.astyanax.Keyspace; import com.netflix.astyanax.serializers.AbstractSerializer; import com.netflix.astyanax.serializers.UUIDSerializer; /** * Version 1 implementation of entity serialization */ @Singleton public class MvccEntitySerializationStrategyV2Impl extends MvccEntitySerializationStrategyImpl { private static final IdRowCompositeSerializer ID_SER = IdRowCompositeSerializer.get(); private static final CollectionScopedRowKeySerializer<Id> ROW_KEY_SER = new CollectionScopedRowKeySerializer<>( ID_SER ); private static final MultiTenantColumnFamily<ScopedRowKey<CollectionPrefixedKey<Id>>, UUID> CF_ENTITY_DATA = new MultiTenantColumnFamily<>( "Entity_Version_Data_V2", ROW_KEY_SER, UUIDSerializer.get() ); private static final FieldBufferSerializer FIELD_BUFFER_SERIALIZER = FieldBufferSerializer.get(); private final EntitySerializer entitySerializer; @Inject public MvccEntitySerializationStrategyV2Impl( final Keyspace keyspace, final SerializationFig serializationFig, final CassandraFig cassandraFig, final MetricsFactory metricsFactory ) { super( keyspace, serializationFig, cassandraFig ); entitySerializer = new EntitySerializer( serializationFig, metricsFactory ); } @Override protected AbstractSerializer<EntityWrapper> getEntitySerializer() { return entitySerializer; } @Override protected MultiTenantColumnFamily<ScopedRowKey<CollectionPrefixedKey<Id>>, UUID> getColumnFamily() { return CF_ENTITY_DATA; } @Override public int getImplementationVersion() { return CollectionDataVersions.BUFFER_SHORT_FIX.getVersion(); } /** * We should only ever create this once, since this impl is a singleton */ public final class EntitySerializer extends AbstractSerializer<EntityWrapper> { private final SmileFactory SMILE_FACTORY = new SmileFactory(); private final ObjectMapper MAPPER = new ObjectMapper( SMILE_FACTORY ); private final Histogram bytesInHistorgram; private final Histogram bytesOutHistorgram; private final Timer bytesOutTimer; private SerializationFig serializationFig; private byte STATE_COMPLETE = 0; private byte STATE_DELETED = 1; private byte STATE_PARTIAL = 2; private byte VERSION = 1; public EntitySerializer( final SerializationFig serializationFig, final MetricsFactory metricsFactory) { this.serializationFig = serializationFig; // SimpleModule listModule = new SimpleModule("ListFieldModule", new Version(1, 0, 0, null,null,null)) // .addAbstractTypeMapping(ListField.class, ArrayField.class); // MAPPER.registerModule(listModule); // causes slowness MAPPER.enableDefaultTypingAsProperty( ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, "@class" ); this.bytesOutHistorgram = metricsFactory.getHistogram(MvccEntitySerializationStrategyV2Impl.class, "bytes.out"); this.bytesInHistorgram = metricsFactory.getHistogram(MvccEntitySerializationStrategyV2Impl.class, "bytes.in"); this.bytesOutTimer = metricsFactory.getTimer(MvccEntitySerializationStrategyV2Impl.class, "bytes.out"); } @Override public ByteBuffer toByteBuffer( final EntityWrapper wrapper ) { if ( wrapper == null ) { return null; } //we always have a max of 3 fields FieldBufferBuilder builder = new FieldBufferBuilder( 3 ); builder.addByte( VERSION ); //mark this version as empty if ( !wrapper.entity.isPresent() ) { //we're empty builder.addByte( STATE_DELETED ); return FIELD_BUFFER_SERIALIZER.toByteBuffer( builder.build() ); } //we have an entity if ( wrapper.status == MvccEntity.Status.COMPLETE ) { builder.addByte( STATE_COMPLETE ); } else { builder.addByte( STATE_PARTIAL ); } final Entity entity = wrapper.entity.get(); final byte[] entityBytes; try { entityBytes = MAPPER.writeValueAsBytes( entity ); } catch ( JsonProcessingException e ) { throw new RuntimeException( "Unable to serialize entity", e ); } final int maxEntrySize = serializationFig.getMaxEntitySize(); if ( entityBytes.length > maxEntrySize ) { throw new EntityTooLargeException( entity, maxEntrySize, entityBytes.length, "Your entity cannot exceed " + maxEntrySize + " bytes. The entity you tried to save was " + entityBytes.length + " bytes" ); } builder.addBytes( entityBytes ); bytesInHistorgram.update(entityBytes.length); return FIELD_BUFFER_SERIALIZER.toByteBuffer( builder.build() ); } @Override public EntityWrapper fromByteBuffer( final ByteBuffer byteBuffer ) { /** * We intentionally turn data corruption exceptions when we're unable to de-serialize * the data in cassandra. If this occurs, we'll never be able to de-serialize it * and it should be considered lost. This is an error that is occurring due to a bug * in serializing the entity. This is a lazy recognition + repair signal for deployment with * existing systems. */ final FieldBuffer fieldBuffer; try { fieldBuffer = FIELD_BUFFER_SERIALIZER.fromByteBuffer( byteBuffer ); } catch ( Exception e ) { throw new DataCorruptionException( "Unable to de-serialze entity", e ); } FieldBufferParser parser = new FieldBufferParser( fieldBuffer ); byte version = parser.readByte(); if ( VERSION != version ) { throw new UnsupportedOperationException( "A version of type " + version + " is unsupported" ); } byte state = parser.readByte(); // it's been deleted, remove it if ( STATE_DELETED == state ) { return new EntityWrapper( MvccEntity.Status.DELETED, Optional.<Entity>absent() ); } Entity storedEntity; byte[] array = parser.readBytes(); try { Timer.Context time = bytesOutTimer.time(); bytesOutHistorgram.update(array == null ? 0 : array.length); storedEntity = MAPPER.readValue( array, Entity.class ); time.stop(); } catch ( Exception e ) { throw new DataCorruptionException( "Unable to read entity data", e ); } final Optional<Entity> entity = Optional.of( storedEntity ); if ( STATE_COMPLETE == state ) { return new EntityWrapper( MvccEntity.Status.COMPLETE, entity ); } // it's partial by default return new EntityWrapper( MvccEntity.Status.PARTIAL, entity ); } } }