2009年10月3日土曜日

#appengine MakeSyncCallServlet

昨日のappengine-java-nightに参加した皆さんなら、エントリのタイトルだけ見たら中身を見る必要はありませんね!

ちょっと今は時間がないのでコードだけうpしますが、クライアント側の環境で通常通りappengineのサービスにアクセスしたら、なぜかデプロイ環境側のサービスにアクセスする、という仕組みが動作しました。

  1. リクエストされたバイト配列とサービス名、メソッド名、アプリケーション名(うっかり間違ったアプリを触るのを防ぐため。)を使ってmeksynccallをするだけのサーブレットを作成し、デプロイ環境にデプロイする
  2. クライアント側では、makeSyncCallへのリクエストを上記のサーブレットへ転送するだけのApiProxy.Delegateを実装し、ApiProxy#setDelegate(ApiProxyLocalImpl)した後で、ApiProxy#setDelegate()する。
  3. クライアント側で普通にデータストアサービスやMemcacheサービスへアクセスする。
  4. それら全てのサービス(UserServiceはそもそもmakeSyncCallを通らないので無理ですが)へのアクセスがサーバ側で実行される!

サンプル

こんなカンジのサンプルで、デプロイ環境側のデータストアに書き込んだり読み込んだり、memcacheのstatisticsを取得したりする事に成功しました。

@Before
public void setUp() throws MalformedURLException {
  ApiProxy.setEnvironmentForCurrentThread(newEnvironment());
  ApiProxy.setDelegate(new ApiProxyLocalImpl(new File("target")) {});
  // MakeSyncCallServletへアクセスするためのDelegateで上書き!
  ApiProxy.setDelegate(new MakeSyncCallServletDelegate(new URL(
      "${アプリのURL}${MakeSyncCallServletのパス}")));
}

@After
public void tearDown() {
  ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ((MakeSyncCallServletDelegate) ApiProxy
      .getDelegate()).getOriginal();
  ApiProxy.setDelegate(null);
  ApiProxy.setEnvironmentForCurrentThread(null);
}

@Test public void runQuery() {
  Query query = new Query("_makeSyncCallTest");
  DatastoreService service = DatastoreServiceFactory.getDatastoreService();
  List<Entity> list = service.prepare(query).asList(FetchOptions.Builder.withOffset(0).limit(1000));
  for (Entity entity : list) {
    System.out.println(ToStringBuilder.reflectionToString(entity));
  }
}

@Test public void put() {
  Entity entity = new Entity("_makeSyncCallTest");
  entity.setProperty("timestamp", new Date(System.currentTimeMillis()));
  DatastoreService service = DatastoreServiceFactory.getDatastoreService();
  service.put(entity);
}

@Test public void statistics() {
  MemcacheService service = MemcacheServiceFactory.getMemcacheService();
  Stats statistics = service.getStatistics();
  System.out.println(statistics);
}

MakeSyncCallServlet

package com.shin1ogawa.servlet;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.wicket.util.io.IOUtils;

import com.google.apphosting.api.ApiProxy;

/**
 * リクエストされたbyte配列を{@link ApiProxy#makeSyncCall(String, String, byte[])}へ引き渡し、
 * その結果をレスポンスするだけのServlet.
 * <p>特殊なサーブレットなので、web.xmlにてセキュリティを設定しておくのがおすすめ。</p>
 * <div><ul>
 * <li>HttpHeaderに以下を設定する。
 * <ul><li>{@literal serviceName}</li><li>{@literal methodName}</li>
 * <li>{@litera applicationId}</li></ul></li>
 * <li>payloadにProtocolBufferで出力されたbyte配列を設定して{@literal POST}する。</li>
 * <li>{@literal application/octet-stream}で
 * {@link ApiProxy#makeSyncCall(String, String, byte[])}の結果を返すので、クライアント側でよしなに。</li>
 * </ul></div>
 * 
 * @author shin1ogawa
 */
public class MakeSyncCallServlet extends HttpServlet {

  private static final long serialVersionUID = 2380791176214953417L;
  private static final String APPLICATION_ID = "applicationId";
  private static final String METHOD_NAME = "methodName";
  private static final String SERVICE_NAME = "serviceName";
  private static Logger logger = Logger.getLogger(MakeSyncCallServlet.class.getName());

  @Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String serviceName = req.getHeader(SERVICE_NAME);
    String methodName = req.getHeader(METHOD_NAME);
    String applicationId = req.getHeader(APPLICATION_ID);
    logger.info(applicationId + ":" + serviceName + "#" + methodName);
    if (validateParameters(resp, serviceName, methodName, applicationId) == false) {
      return;
    }
    byte[] requestBytes = IOUtils.toByteArray(req.getInputStream());
    logger.info(applicationId + ":" + serviceName + "#" + methodName + ": requestBytes.length="
        + (requestBytes != null ? requestBytes.length : "null"));
    byte[] responseBytes = null;
    try {
      @SuppressWarnings("unchecked")
      byte[] bytes = ApiProxy.getDelegate().makeSyncCall(ApiProxy.getCurrentEnvironment(),
          serviceName, methodName, requestBytes);
      responseBytes = bytes;
    } catch (Throwable th) {
      logger.log(Level.WARNING, applicationId + ":" + serviceName + "#" + methodName, th);
      resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
      resp.setContentType("text/plain");
      th.printStackTrace(resp.getWriter());
      resp.getWriter().flush();
      return;
    }
    if (responseBytes == null) {
      logger.info(serviceName + "#" + methodName + ": responseBytes == null.");
      responseBytes = new byte[0];
    } else {
      logger.info(serviceName + "#" + methodName + ": responseBytes.length="
          + responseBytes.length);
    }
    resp.setContentType("application/octet-stream");
    resp.getOutputStream().write(responseBytes);
    resp.getOutputStream().flush();
  }

  private boolean validateParameters(HttpServletResponse resp, String serviceName,
      String methodName, String applicationId) throws IOException {
    if (StringUtils.isEmpty(serviceName)) {
      onErrorInParameters(resp, "serviceName was not specified.");
      return false;
    }
    if (StringUtils.isEmpty(methodName)) {
      onErrorInParameters(resp, "methodName was not specified.");
      return false;
    }
    if (StringUtils.isEmpty(applicationId)) {
      onErrorInParameters(resp, "applicationId was not specified.");
      return false;
    }
    // 念のためサーバ環境のApplicationIdと同じかどうか確認する。
    String serverApplicationId = ApiProxy.getCurrentEnvironment().getAppId();
    if (serverApplicationId.equals(applicationId) == false) {
      onErrorInParameters(resp, "applicationId was not equals to " + serverApplicationId
          + ".");
      return false;
    }
    return true;
  }

  private void onErrorInParameters(HttpServletResponse resp, String x) throws IOException {
    resp.setStatus(HttpURLConnection.HTTP_INTERNAL_ERROR);
    resp.setContentType("text/plain");
    resp.getWriter().println(x);
    resp.getWriter().flush();
  }
}

MakeSyncCallServletDelegate

Http通信の手を抜くためにURLFetchServiceを使ってるけど、commons-httpclientに置き換えるつもり。

public class MakeSyncCallServletDelegate implements ApiProxy.Delegate<Environment> {

  @SuppressWarnings("unchecked")
  private final ApiProxy.Delegate<Environment> original = ApiProxy.getDelegate();

  final URL url;

  MakeSyncCallServletDelegate(URL url) throws MalformedURLException {
    this.url = url;
  }

  public void log(Environment environment, LogRecord logRecord) {
    getOriginal().log(environment, logRecord);
  }

  public byte[] makeSyncCall(Environment environment, String serviceName, String methodName,
      byte[] request) throws ApiProxyException {
    if (serviceName.equals("urlfetch")) {
      return original.makeSyncCall(environment, serviceName, methodName, request);
    }
    URLFetchService service = URLFetchServiceFactory.getURLFetchService();
    try {
      HTTPRequest httpRequest = new HTTPRequest(url, HTTPMethod.POST);
      httpRequest.addHeader(new HTTPHeader("serviceName", serviceName));
      httpRequest.addHeader(new HTTPHeader("methodName", methodName));
      httpRequest.addHeader(new HTTPHeader("applicationId", environment.getAppId()));
      httpRequest.setPayload(request);
      HTTPResponse httpResponse = service.fetch(httpRequest);
      if (httpResponse.getResponseCode() == 500) {
        System.out.println(new String(httpResponse.getContent()));
        return new byte[0];
      }
      return httpResponse.getContent();
    } catch (IOException e) {
      e.printStackTrace();
      return new byte[0];
    }
  }

  public ApiProxy.Delegate<Environment> getOriginal() {
    return original;
  }
}

コメントを投稿