package org.rakam.recipe; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.rakam.analysis.ConfigManager; import org.rakam.analysis.ContinuousQueryService; import org.rakam.analysis.MaterializedViewService; import org.rakam.analysis.metadata.Metastore; import org.rakam.analysis.metadata.SchemaChecker; import org.rakam.collection.FieldType; import org.rakam.collection.SchemaField; import org.rakam.plugin.ContinuousQuery; import org.rakam.plugin.MaterializedView; import org.rakam.report.QueryResult; import org.rakam.util.AlreadyExistsException; import org.rakam.util.RakamException; import javax.inject.Inject; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; import static java.lang.String.format; import static org.rakam.analysis.InternalConfig.USER_TYPE; import static org.rakam.collection.FieldType.STRING; import static org.rakam.report.QueryError.create; public class RecipeHandler { private final Metastore metastore; private final ContinuousQueryService continuousQueryService; private final MaterializedViewService materializedViewService; private final ConfigManager configManager; private final SchemaChecker schemaChecker; @Inject public RecipeHandler( Metastore metastore, ContinuousQueryService continuousQueryService, ConfigManager configManager, SchemaChecker schemaChecker, MaterializedViewService materializedViewService) { this.metastore = metastore; this.configManager = configManager; this.schemaChecker = schemaChecker; this.materializedViewService = materializedViewService; this.continuousQueryService = continuousQueryService; } public Recipe export(String project) { final Map<String, Recipe.CollectionDefinition> collections = metastore.getCollections(project).entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> { List<Map<String, Recipe.SchemaFieldInfo>> map = e.getValue().stream() .map(a -> ImmutableMap.of(a.getName(), new Recipe.SchemaFieldInfo(a.getCategory(), a.getType()))) .collect(Collectors.toList()); return new Recipe.CollectionDefinition(map); })); final List<MaterializedView> materializedViews = materializedViewService.list(project).stream() .map(m -> new MaterializedView(m.tableName, m.name, m.query, m.updateInterval, m.incremental, m.realTime, m.options)) .collect(Collectors.toList()); final List<ContinuousQuery> continuousQueryBuilders = continuousQueryService.list(project).stream() .map(m -> new ContinuousQuery(m.tableName, m.name, m.query, m.partitionKeys, m.options)) .collect(Collectors.toList()); return new Recipe(Recipe.Strategy.SPECIFIC, collections, materializedViews, continuousQueryBuilders); } public void install(Recipe recipe, String project, boolean overrideExisting) { installInternal(recipe, project, overrideExisting); } public void install(String project, Recipe recipe, boolean overrideExisting) { installInternal(recipe, project, overrideExisting); } public void installInternal(Recipe recipe, String project, boolean overrideExisting) { recipe.getCollections().forEach((collectionName, collection) -> { List<SchemaField> build = collection.build().stream() .map(e -> { FieldType type; if (e.getName().equals("_user")) { type = configManager.setConfigOnce(project, USER_TYPE.name(), STRING); } else { type = e.getType(); } SchemaField schemaField = new SchemaField(e.getName(), type, e.isUnique(), e.getDescriptiveName(), e.getDescription(), e.getCategory()); return schemaField; }) .collect(Collectors.toList()); HashSet<SchemaField> schemaFields = schemaChecker.checkNewFields(collectionName, ImmutableSet.copyOf(build)); List<SchemaField> fields = metastore.getOrCreateCollectionFieldList(project, collectionName, schemaFields); List<SchemaField> collisions = build.stream() .filter(f -> fields.stream().anyMatch(field -> field.getName().equals(f.getName()) && !f.getType().equals(field.getType()))) .collect(Collectors.toList()); if (!collisions.isEmpty()) { String errMessage = collisions.stream().map(f -> { SchemaField existingField = fields.stream().filter(field -> field.getName().equals(f.getName())).findAny().get(); return format("Recipe: [%s : %s], CollectionDefinition: [%s, %s]", f.getName(), f.getType(), existingField.getName(), existingField.getType()); }).collect(Collectors.joining(", ")); String message = overrideExisting ? "Overriding collection fields is not possible." : "Collision in collection fields."; throw new RakamException(message + " " + errMessage, BAD_REQUEST); } }); List<CompletableFuture<QueryResult>> continuousQueries = recipe.getContinuousQueryBuilders().stream() .map(continuousQuery -> continuousQueryService.create(project, continuousQuery, false).getResult().handle((res, ex) -> { if (ex != null) { if (ex.getCause() instanceof AlreadyExistsException) { if (overrideExisting) { try { continuousQueryService.delete(project, continuousQuery.tableName).join(); return continuousQueryService.create(project, continuousQuery, false).getResult().join(); } catch (Throwable e) { return QueryResult.errorResult( create(format("Error while re-creating materialized view %s: %s", continuousQuery.getTableName(), ex.getMessage()))); } } else { return QueryResult.errorResult(create(format("Continuous query %s already exists", continuousQuery.getTableName()))); } } return QueryResult.errorResult( create(format("Error while creating materialized view %s: %s", continuousQuery.getTableName(), ex.getMessage()))); } else { return res; } })).collect(Collectors.toList()); List<CompletableFuture<QueryResult>> materializedViews = recipe.getMaterializedViewBuilders().stream() .map(materializedView -> materializedViewService.create(project, materializedView).handle((res, ex) -> { if (ex != null) { if (ex instanceof AlreadyExistsException) { if (overrideExisting) { try { materializedViewService.delete(project, materializedView.tableName); materializedViewService.create(project, materializedView); return QueryResult.empty(); } catch (Throwable e) { return QueryResult.errorResult( create(format("Error while re-creating materialized view %s: %s", materializedView.tableName, e.getMessage()))); } } else { return QueryResult.errorResult(create(format("Materialized view %s already exists", materializedView.tableName))); } } return QueryResult.errorResult( create(format("Error while creating materialized view %s: %s", materializedView.tableName, ex.getMessage()))); } else { return QueryResult.empty(); } })).collect(Collectors.toList()); CompletableFuture<QueryResult>[] futures = ImmutableList.builder().addAll(continuousQueries) .addAll(materializedViews).build().stream() .toArray(CompletableFuture[]::new); CompletableFuture.allOf(futures).join(); String errors = Arrays.stream(futures) .map(e -> e.join()) .filter(e -> e.isFailed()) .map(e -> e.getError().toString()) .collect(Collectors.joining("\n")); if (!errors.isEmpty()) { throw new RakamException(errors, INTERNAL_SERVER_ERROR); } } }