/*
* 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.core.migration.data;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.usergrid.persistence.core.migration.schema.MigrationException;
import com.google.common.base.Preconditions;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
public class DataMigrationManagerImpl implements DataMigrationManager {
private static final Logger logger = LoggerFactory.getLogger( DataMigrationManagerImpl.class );
private final Map<String, MigrationPlugin> migrationPlugins;
private final List<MigrationPlugin> executionOrder;
private final MigrationInfoSerialization migrationInfoSerialization;
private final MigrationInfoCache migrationInfoCache;
@Inject
public DataMigrationManagerImpl( final Set<MigrationPlugin> plugins,
final MigrationInfoSerialization migrationInfoSerialization,
final MigrationInfoCache migrationInfoCache
) {
this.migrationInfoCache = migrationInfoCache;
Preconditions.checkNotNull( plugins, "plugins must not be null" );
Preconditions.checkNotNull( migrationInfoSerialization, "migrationInfoSerialization must not be null" );
this.migrationInfoSerialization = migrationInfoSerialization;
this.executionOrder = new ArrayList<>(plugins.size());
this.migrationPlugins = new HashMap<>();
for ( MigrationPlugin plugin : plugins ) {
final String name = plugin.getName();
final MigrationPlugin existing = migrationPlugins.get( name );
if ( existing != null ) {
throw new IllegalArgumentException( "Duplicate plugin name detected. A plugin with name " + name
+ " is already implemented by class '" + existing.getClass().getName() + "'. Class '" + plugin
.getClass().getName() + "' is also trying to implement this name." );
}
this.migrationPlugins.put( name, plugin );
this.executionOrder.add( plugin );
}
//now sort based on execution order
Collections.sort(executionOrder, MigrationPluginComparator.INSTANCE);
}
@Override
public boolean pluginExists(final String name) {
return migrationPlugins.containsKey(name);
}
@Override
public void migrate(final String name) throws MigrationException {
/**
* Invoke each plugin to attempt a migration
*/
final MigrationPlugin plugin = migrationPlugins.get( name );
if(plugin != null){
final ProgressObserver observer = new CassandraProgressObserver(plugin.getName());
plugin.run(observer);
migrationInfoCache.invalidateAll();
}else {
throw new IllegalArgumentException(name + " does not match a current plugin.");
}
}
@Override
public void migrate() throws MigrationException {
/**
* Invoke each plugin to attempt a migration
*/
executionOrder.forEach(plugin -> {
final ProgressObserver observer = new CassandraProgressObserver(plugin.getName());
plugin.run(observer);
migrationInfoCache.invalidateAll();
});
}
@Override
public boolean isRunning() {
//we have to get our state from cassandra
for(final String pluginName :getPluginNames()){
if( migrationInfoSerialization.getStatusCode(pluginName) == StatusCode.RUNNING.status){
return true;
}
}
return false;
}
@Override
public int getCurrentVersion( final String pluginName ) {
Preconditions.checkNotNull( pluginName, "pluginName cannot be null" );
return migrationInfoSerialization.getVersion( pluginName );
}
@Override
public void resetToVersion( final String pluginName, final int version ) {
Preconditions.checkNotNull( pluginName, "pluginName cannot be null" );
final MigrationPlugin plugin = migrationPlugins.get( pluginName );
Preconditions.checkArgument( plugin != null, "Plugin " + pluginName + " could not be found" );
final int highestAllowed = plugin.getMaxVersion();
Preconditions.checkArgument( version <= highestAllowed,
"You cannot set a version higher than the max of " + highestAllowed + " for plugin " + pluginName );
Preconditions.checkArgument( version >= 0, "You must specify a version of 0 or greater" );
migrationInfoSerialization.setVersion( pluginName, version );
migrationInfoCache.invalidateAll();
}
@Override
public String getLastStatus( final String pluginName ) {
Preconditions.checkNotNull( pluginName, "pluginName cannot be null" );
return migrationInfoSerialization.getStatusMessage( pluginName );
}
@Override
public Set<String> getPluginNames() {
return migrationPlugins.keySet();
}
/**
* Different status enums
*/
public enum StatusCode {
COMPLETE( 1 ),
RUNNING( 2 ),
ERROR( 3 );
public final int status;
StatusCode( final int status ) {this.status = status;}
}
public List<MigrationPlugin> getExecutionOrder(){return executionOrder;}
private final class CassandraProgressObserver implements ProgressObserver {
private final String pluginName;
private boolean failed = false;
private CassandraProgressObserver( final String pluginName ) {this.pluginName = pluginName;}
@Override
public void start() {
migrationInfoSerialization.setStatusCode( pluginName, StatusCode.RUNNING.status );
}
@Override
public void complete() {
migrationInfoSerialization.setStatusCode( pluginName, StatusCode.COMPLETE.status );
}
@Override
public void failed( final int migrationVersion, final String reason ) {
final String storedMessage = String.format( "Failed to migrate, reason is appended. Error '%s'", reason );
update( migrationVersion, storedMessage );
logger.error( storedMessage );
failed = true;
migrationInfoSerialization.setStatusCode( pluginName, StatusCode.ERROR.status );
}
@Override
public void failed( final int migrationVersion, final String reason, final Throwable throwable ) {
StringWriter stackTrace = new StringWriter();
throwable.printStackTrace( new PrintWriter( stackTrace ) );
//todo shouldn't we log this? I added it so we have a record.
logger.error("Data Migration Manager processing failed", throwable);
final String storedMessage = String.format( "Failed to migrate, reason is appended. Error '%s' %s", reason,
stackTrace.toString() );
update( migrationVersion, storedMessage );
logger.error( "Unable to migrate version {} due to reason {}.", migrationVersion, reason, throwable );
failed = true;
migrationInfoSerialization.setStatusCode( pluginName, StatusCode.ERROR.status );
}
@Override
public void update( final int migrationVersion, final String message ) {
final String formattedOutput = String.format( "Migration version %d. %s", migrationVersion, message );
//Print this to the info log
logger.info( formattedOutput );
migrationInfoSerialization.setStatusMessage( pluginName, formattedOutput );
}
/**
* Return true if we failed
*/
public boolean isFailed() {
return failed;
}
}
private final static class MigrationPluginComparator implements Comparator<MigrationPlugin> {
public static final MigrationPluginComparator INSTANCE = new MigrationPluginComparator();
@Override
public int compare( final MigrationPlugin o1, final MigrationPlugin o2 ) {
//first one is less
if(o1.getPhase().ordinal() < o2.getPhase().ordinal()){
return -1;
}
//second one is first
if(o2.getPhase().ordinal() < o1.getPhase().ordinal()){
return 1;
}
//if our phase for
return o1.getName().compareTo( o2.getName() );
}
}
}