Package com.goodow.realtime.store.server.impl

Source Code of com.goodow.realtime.store.server.impl.OperationProcessor

/*
* Copyright 2014 Goodow.com
*
* Licensed 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 com.goodow.realtime.store.server.impl;

import com.google.inject.Inject;

import com.goodow.realtime.json.impl.JreJsonArray;
import com.goodow.realtime.json.impl.JreJsonObject;
import com.goodow.realtime.operation.Transformer;
import com.goodow.realtime.operation.impl.CollaborativeOperation;
import com.goodow.realtime.store.channel.Constants.Key;
import com.goodow.realtime.store.impl.DocumentBridge;
import com.goodow.realtime.store.server.DeltaStorage;

import org.vertx.java.core.AsyncResult;
import org.vertx.java.core.AsyncResultHandler;
import org.vertx.java.core.eventbus.ReplyException;
import org.vertx.java.core.eventbus.ReplyFailure;
import org.vertx.java.core.impl.DefaultFutureResult;
import org.vertx.java.core.json.JsonArray;
import org.vertx.java.core.json.JsonObject;

import java.util.logging.Level;
import java.util.logging.Logger;

public class OperationProcessor {
  private static final Logger log  = Logger.getLogger(OperationProcessor.class.getName());

  @SuppressWarnings("unchecked")
  public static DocumentBridge createSnapshot(final String docType, final String docId,
                                              JsonObject snapshotData) {
    JreJsonArray snapshot = snapshotData.containsField(Key.SNAPSHOT)
                            ? new JreJsonArray(snapshotData.getArray(Key.SNAPSHOT).toList()) : null;
    DocumentBridge bridge = new DocumentBridge(null, docType + "/" + docId, snapshot, null, null);
    return bridge;
  }

  @Inject private Transformer<CollaborativeOperation> transformer;
  @Inject private DeltaStorage storage;

  /**
   * Submit an operation on the named docType/docId document.
   *
   * @param opData should probably contain a v: field (if it doesn't, it defaults to the current
   *               version).
   * @param callback called with {v:, ops:[], snapshot:{}}
   */
  public void submit(String docType, String docId, final JsonObject opData,
                     final AsyncResultHandler<JsonObject> callback) {
    retrySubmit(new JsonArray(), docType, docId, createOperation(opData), opData
        .getLong(Key.VERSION), callback);
  }

  private CollaborativeOperation createOperation(JsonObject opData) {
    return transformer.createOperation(new JreJsonObject(opData.toMap()));
  }

  private JreJsonArray createOperations(JsonArray opDatas, int from, int to) {
    JreJsonArray operations = new JreJsonArray();
    for (; from < to; from++) {
      operations.push(createOperation(opDatas.<JsonObject> get(from)));
    }
    return operations;
  }

  /**
   * Great - now we're in the situation that we can actually submit the operation to the database.
   * If this method succeeds, it should update any persistant oplogs before calling the callback to
   * tell us about the successful commit. I could make this API more complicated, enabling the
   * function to return actual operations and whatnot, but its quite rare to actually need to
   * transform data on the server at this point.
   */
  private void doSubmit(final JsonArray transformedOps, final String docType,
                        final String docId, final CollaborativeOperation operation, final long applyAt,
                        final DocumentBridge snapshot, final AsyncResultHandler<JsonObject> callback) {
    final JsonObject opData =
        new JsonObject(((JreJsonObject) operation.toJson()).toNative()).putNumber(Key.VERSION,
                                                                                  applyAt);
    storage.atomicSubmit(docType, docId, opData, new AsyncResultHandler<Void>() {
      @Override
      public void handle(AsyncResult<Void> ar) {
        if (ar.failed()) {
          if ("Transform needed".equals(ar.cause().getMessage())) {
            // Between our fetch and our call to atomicSubmit, another client
            // submitted an operation. This should be pretty rare. Calling
            // retry() here will re-fetch the snapshot again (not necessary),
            // but its a rare enough case that its not worth optimizing.
            retrySubmit(transformedOps, docType, docId, operation, applyAt, callback);
          } else {
            callback.handle(new DefaultFutureResult<JsonObject>(ar.cause()));
          }
          return;
        }
        log.finest("Wrote op @" + applyAt);
        final JsonObject root = new JsonObject(((JreJsonObject) snapshot.toJson()).toNative());
        JsonObject snapshotData = new JsonObject().putNumber(Key.VERSION, applyAt + 1)
            .putObject(DeltaStorage.ROOT, root).putArray(
                Key.SNAPSHOT, new JsonArray(((JreJsonArray) snapshot.toSnapshot()).toNative()));
        writeSnapshotAfterSubmit(docType, docId, snapshotData, opData,
                                 new AsyncResultHandler<Void>() {
          @Override
          public void handle(AsyncResult<Void> ar) {
            // What do we do if the snapshot write fails? We've already committed the operation -
            // its done and dusted. We probably shouldn't re-run polling queries now. Really, no
            // matter what we do here things are going to be a little bit broken, depending on the
            // behaviour we trap in finish.

            // Its sort of too late to error out if the snapshotdb can't take our op - the op has
            // been commited.

            // postSubmit is for things like publishing the operation over pubsub. We should
            // probably make this asyncronous.
            storage.postSubmit(docType, docId, opData, root);
            log.finest("Wrote snapshot @" + (applyAt + 1));
            callback.handle(new DefaultFutureResult<JsonObject>(new JsonObject().putNumber(
                Key.VERSION, applyAt).putArray(Key.OPS, transformedOps).putObject(
                Key.SNAPSHOT, root)));
          }
        });
      }
    });
  }

  /**
   * This is a fetch that doesn't check the oplog to see if the snapshot is out of date. It will be
   * higher performance, but in some error conditions it may return an outdated snapshot.
   */
  private void lazyFetch(String docType, String docId,
                         final AsyncResultHandler<JsonObject> callback) {
    storage.getSnapshot(docType, docId, null, new AsyncResultHandler<JsonObject>() {
      @Override
      public void handle(AsyncResult<JsonObject> ar) {
        if (ar.succeeded() && ar.result() == null) {
          callback.handle(new DefaultFutureResult<JsonObject>(new JsonObject().putNumber(
              Key.VERSION, 0)));
          return;
        }
        callback.handle(ar);
      }
    });
  }

  private void retrySubmit(final JsonArray transformedOps, final String docType,
                           final String docId, final CollaborativeOperation operation, final Long applyAt,
                           final AsyncResultHandler<JsonObject> callback) {
    // First we'll get a doc snapshot. This wouldn't be necessary except that we need to check that
    // the operation is valid against the current document before accepting it.
    lazyFetch(docType, docId, new AsyncResultHandler<JsonObject>() {
      @Override
      public void handle(AsyncResult<JsonObject> ar) {
        if (ar.failed()) {
          callback.handle(ar);
          return;
        }
        final JsonObject snapshotData = ar.result();
        final long snapshotVersion = snapshotData.getLong(Key.VERSION);
        // Get all operations that might be relevant. We'll float the snapshot and the operation up
        // to the most recent version of the document, then try submitting.
        final long from = (applyAt != null && applyAt < snapshotVersion) ? applyAt : snapshotVersion;
        storage.getOps(docType, docId, from, null, new AsyncResultHandler<JsonObject>() {
          @Override
          public void handle(AsyncResult<JsonObject> ar) {
            if (ar.failed()) {
              callback.handle(ar);
              return;
            }
            final DocumentBridge snapshot = createSnapshot(docType, docId, snapshotData);
            JsonArray ops = ar.result().getArray(Key.OPS);
            if (ops.size() > 0) {
              log.finest("Transform Needed");
            }
            long snapshotV = snapshotVersion;
            long opV = applyAt == null ? snapshotV + ops.size() : applyAt;
            for (Object op : ops) {
              JsonObject opData = (JsonObject) op;
              // if (opData.containsField("seq") && opData.getString("sid") == op.getString("sid")
              // && opData.getLong("seq") == op.getLong("seq")) {
              // // The op has already been submitted. There's a variety of ways this can happen.
              // Its
              // // important we don't transform it by itself & submit again.
              // callback.handle(new DefaultFutureResult<JsonObject>(new ReplyException(
              // ReplyFailure.RECIPIENT_FAILURE, "Op already submitted")));
              // return;
              // }

              // Bring both the op and the snapshot up to date. At least one of these two
              // conditionals should be true.
              long opVersion = opData.getLong(Key.VERSION);
              if (snapshotV == opVersion) {
                try {
                  snapshot.consume(createOperation(opData));
                } catch (Exception e) {
                  callback.handle(new DefaultFutureResult<JsonObject>(new ReplyException(
                      ReplyFailure.RECIPIENT_FAILURE, e.getMessage())));
                  return;
                }
                snapshotV++;
              }
              if (opV == opVersion) {
                transformedOps.add(opData);
                opV++;
              }
            }
            if (opV != snapshotV) {
              callback.handle(new DefaultFutureResult<JsonObject>(new ReplyException(
                  ReplyFailure.RECIPIENT_FAILURE, "Invalid opData version")));
              return;
            }
            CollaborativeOperation transformed = operation;
            if (applyAt != null && ops.size() > 0
                && applyAt <= ops.<JsonObject>get(ops.size() - 1).getLong(Key.VERSION)) {
              try {
                CollaborativeOperation applied =
                    transformer.compose(createOperations(ops, (int) (applyAt - ops
                        .<JsonObject>get(0).getLong(Key.VERSION)), ops.size()));
                transformed = operation.transform(applied, false);
              } catch (Exception e) {
                log.log(Level.WARNING, "Failed to transform operation", e);
                callback.handle(new DefaultFutureResult<JsonObject>(new ReplyException(
                    ReplyFailure.RECIPIENT_FAILURE, e.getMessage())));
                return;
              }
            }

            // Ok, now we can try to apply the op.
            try {
              snapshot.consume(transformed);
            } catch (Exception e) {
              log.log(Level.WARNING, "Failed to consume operation", e);
              callback.handle(new DefaultFutureResult<JsonObject>(new ReplyException(
                  ReplyFailure.RECIPIENT_FAILURE, e.getMessage())));
              return;
            }
            doSubmit(transformedOps, docType, docId, transformed, opV, snapshot, callback);
          }
        });
      }
    });
  }

  private void writeSnapshotAfterSubmit(String docType, String docId, JsonObject snapshotData,
                                        JsonObject opData, AsyncResultHandler<Void> callback) {
    storage.writeSnapshot(docType, docId, snapshotData, callback);
  }
}
TOP

Related Classes of com.goodow.realtime.store.server.impl.OperationProcessor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.