ラベル GAE の投稿を表示しています。 すべての投稿を表示
ラベル GAE の投稿を表示しています。 すべての投稿を表示

2011年2月7日月曜日

[GAE] bulkloaderをGAE/Jで使う

このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
python版appengine-sdkを使ってcsvファイルからデータをDatastoreにロードすることができます。

準備

web.xmlにremote_api用のServletの設定を書く

<servlet>
  <servlet-name>remoteapi</servlet-name>
  <servlet-class>com.google.apphosting.utils.remoteapi.RemoteApiServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>remoteapi</servlet-name>
  <url-pattern>/remote_api</url-pattern>
</servlet-mapping>
security-constraintも追加しておいた方が良さげ。
<security-constraint>
  <web-resource-collection>
    <url-pattern>/remote_api</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <role-name>admin</role-name>
  </auth-constraint>
</security-constraint>

ロード設定用のyamlファイルを書く

python_preamble:
- import: base64
- import: re
- import: google.appengine.ext.bulkload.transform
- import: google.appengine.ext.bulkload.bulkloader_wizard
- import: google.appengine.api.datastore
- import: google.appengine.api.users

transformers:

- kind: Scores
  connector: csv

  connector_options:
    encoding: ms932

  property_map:
    - property: __key__
      external_name: id
      import_transform: int

    - property: name
      external_name: name

    - property: score
      external_name: score
      import_transform: transform.none_if_empty(float)
  • キーにidではなく文字列(name)を使用する場合は、import_transform: intは不要。
  • import_transformに他に指定できる例
    日付: transform.import_date_time('%Y/%m/%d %H:%M')
    固定値: import_transform: lambda x:1
    数値変換: import_transform: lambda x:int(x)*100
    ※lambdaは引数の:の後にスペースを空けるとダメな様です

    ロード用CSV
    id,name,score
    1,Alice,5.0
    2,Bob,2.5
    3,Caroline,
    

    ロード用コマンド

    appcfg.py upload_data --filename=test.csv --config_file=test.yml --url=http://{appid}.appspot.com/remote_api --application={appid} --kind=Scores -v
    
    ※{appid}はアプリケーションIDに置き換える

    security-constraintの設定をしている場合、毎回管理者アドレス/パスワードの入力が必要です。
    ローカルのdev_appserverだとエラーが出て動きませんでした…

    こんな感じに反映されます。
    ※1万件だと約500秒(8分ちょっと)くらいかかりました(CPU Timeを無料Quotaの10%ほど消費します)

    その他注意点

  • slim3を使っている場合、filterの設定があると、"ProtocolBufferDecodeError: corrupted"の様なエラーが出てしまうので一時的にコメントアウトして回避しました。(※他にいい方法があれば教えて下さい…

    2/11追記: bulkload用にfilterをコメントアウトしたversionをデプロイしておいて、--url=http://latest.bulkload.{appid}.appspot.com/remote_api みたいに指定するといいかも

    3/22追記:slim3ではなくscenic3の方の設定でした。matcherで/remote_apiを除外したらOK。

    参考: Ikai Lan says / Using the bulkloader with Java App Engine
  • 2011年1月1日土曜日

    [Scala] sbt-appengineなプロジェクトをIntelliJ IDEAでデバッグ

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    @teaplanetさんにヒントいただいてIDEAのsbt consoleでdev-appserver-startできるようになったので、IDEA上でブレークポイントで止めたりできるように設定してみた。

    class MyProject(info: ProjectInfo) extends AppengineProject(info) {
      //... 中略
      //APPENGINE_SDK_HOME設定の代わり
      override val appengineSdkPath = Path.fromFile("/Users/pomu0325/dev/appengine-java-sdk-1.4.0")  
      
      //dev-appserver-startの際のJVMオプション
      override val devAppserverJvmOptions = List("-Xdebug", "-Xrunjdwp:transport=dt_socket,server=y,address=2011,suspend=y") ++ super.devAppserverJvmOptions 
    }
    

    この設定でsbt consoleからdev-appserver-startするとリモートデバッガで接続できる状態で起動するので、後はIDEAからデバッガ接続する。

    IDEAのメニューからRun -> Edit Configurations -> [+] -> Remote を選んで、Nameは適当に、PortにdevAppserverJvmOptionsのaddress=xxxxで指定したポートを入れる(↑の例だと2011。適当に開いてるポート設定すればOK)。

    設定したら、Run -> Debug... で選んでデバッグ開始するとdev-appserverのJVMにアタッチされてブレークポイントで止まってくれる。

    ※Before launchの"Run sbt action"でdev-appserver-startが指定できればsbt consoleからいちいちdev-appserver-startしなくても一発で起動できそうだけど一覧に出てこないし、直接指定してもダメでした…

    参考:sbt-appengineの基本的な設定等は"sbtでGAEをする場合の注意点など色々。"が参考になります。

    2010年9月6日月曜日

    [Lift][GAE] Scala座01でLTしてきました

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    第1回Scala座に参加してきました。実はスタッフとして少々お手伝いもしたのですが大した事ないので(ry

    収穫は…
    • いろいろと勉強不足だなぁと刺激を受けた(FP的な意味で)
    • Twitter等で気になっていた人々にリアルで会えた
    • ワラビモチ!←マイブーム
    といったとこでしょうか。

    「Lift on GAE/J」ネタでLTさせてもらったのですが、先にまとめてしまうと、「LiftはGoogle AppEngineには向いてない」てことです。詳しくは「Lift on GAE/J」のスライド見てください。

    LTやろうと思ったきっかけは、自分でLiftも(Scalaも)GAEもよく分かってないうちにLift+GAEという組み合わせを選んでしまったのですが、いろいろと苦労したので… 最近TwitterでLiftをGAEで動かしてみるとかのtweetを時々見かけ、危険だよー、ということを広めたかっただけです。

    ただ、勘違いしないで欲しいのは、Liftがダメと言っている訳ではなく、LiftはフルスタックのWebフレームワークとして、慣れれば生産性よくAJAX/Cometを使ったリッチなWebアプリを作れる素晴らしいフレームワークだと思っています。クラウドという性質上、GAEには様々な制限があるので、GAE上で動かしたいのであれば他のもう少し軽量なフレームワークが向いていると思います(まだあまり試せていませんが…)。

    Scala座の企画をして下さった@keisuke_nさん、Scalaを勉強していく上で参考にさせていただいたり助けていただいたりで本当にいろいろとお世話になっている@yuroyoroさん、@kmizuさん、@takedasoftさん、会場・懇親会でお話させていただいたみなさま、本当にありがとうございました。ぜひ第2回でもよろしくお願いします!

    2010年3月22日月曜日

    [Lift][GAE] spin-up時間を測定してみる

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    @yuroyoroさんがGAE上のScalaのスピンアップ時間を測定していたので、倣ってLiftのspin-up時間を測定してみた。前は数回適当に起動した時間を見ていて7-8秒って感じだったのを、もうちょっと正確に。

    測定したのはarchetype:blankで生成される、LiftテンプレートとSnippetを使用したHelloWorld。また、不要なjarを減らしたら早くなるのかも気になったのでいくつかパターンを用意してみた。

    結果

    appavemaxmin
    lift-blank *1682973385938
    nomapper *2668171056035
    nomapper/nojson *3653171245705
    min *4648172405957
    1. Lift2.0-M3のarchetype-blankをそのままGAEに載せたもの
    2. GAE上ではあまり使わないと思われるlift-mapperのjarを取り除いたもの
    3. ↑からさらにlift-jsonのjarを取り除いたもの
    4. ↑からさらにHelloWorldには不要なjar(javax.mail, commons-codec)を全て取り除いたもの

    考察

    • 簡単なLiftテンプレート+Snippetだと、Lift単品では7秒弱。
    • maxとminが前後しているのは気になるが、平均で見ると不要なjarを取り除くと若干早くなるっぽい。
      ※前測って7-8秒だったのは、Lift以外のライブラリのjarがあったのでさらにオーバーヘッドがかかったのか?

    ほか気づいたこと

    Liftがリクエストの処理にかかった時間をログに出してくれるのだが、初回がやたら遅くて3秒ほどかかっているのでこれがspin-upの半分を占めている。2回目のリクエストからは数ms…。
    INFO - Service request (GET) / took 3056 Milliseconds
    次はこれを調べてみることにする。

    2010年3月17日水曜日

    [GAE] JavaでAppstatsを使ってみる

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    3/26追記:やっと公式にドキュメントが追加されました

    GAE SDK 1.3.2より、JavaでもAppstatsが使えるようになりました。1.3.2-preの段階ではまだlabsのjarに入っているので、appengine-api-labs-1.3.2.jarをWEB-INF/libに含めて、web.xmlに↓こんな感じで設定するだけ。※この設定だとstats自身もフィルタ対象になってしまうので除外する方法調査中。
    <filter>
      <filter-name>AppstatsFilter</filter-name>
      <display-name>Appstats Filter</display-name>
      <description>Appstats Filter</description>
      <filter-class>com.google.appengine.tools.appstats.AppstatsFilter</filter-class>
    </filter>
    
    <filter-mapping>
      <filter-name>AppstatsFilter</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>
    
    <servlet>
      <servlet-name>AppstatsServlet</servlet-name>
      <servlet-class>
        com.google.appengine.tools.appstats.AppstatsServlet
      </servlet-class>
    </servlet>
    
    <servlet-mapping>
      <servlet-name>AppstatsServlet</servlet-name>
      <url-pattern>/stats/*</url-pattern>
    </servlet-mapping>
    

    ブラウザで/stats/でを開くと、↓こんな統計が見れます。
    ※/stats て最後のスラッシュを抜かすとなぜかリダイレクトループしてしまいます…
    ※IE8だと表示が崩れてました。

    これだけだとURLが分かれば誰でも見えてしまうので、管理者のみアクセスできるような設定もした方がよいでしょう。
    <security-constraint>
      <web-resource-collection>
        <url-pattern>/stats/*</url-pattern>
      </web-resource-collection>
      <auth-constraint>
        <role-name>admin</role-name>
      </auth-constraint>
    </security-constraint>
    

    [Lift][GAE] DataStoreに溜まったsessionをcronで削除する

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    ↓こんなobjectを作っておき、cronを設定した。だいたい550件程度削除すると30秒経過のタイムアウトでエラーになるけど別に気にしない。
    import net.liftweb.http._
    import net.liftweb.common._
    import com.google.appengine.api.datastore._
    
    object SessionCleaner {
      // Iteratorのimplicit conversionは定義されていないようなので自前で定義。
      implicit def j2s[A](j: java.util.Iterator[A]) = 
        new scala.collection.jcl.MutableIterator.Wrapper[A](j)
      private lazy val DSS = DatastoreServiceFactory.getDatastoreService
    
      def execute(): Box[LiftResponse] = {
        var count = 0
        try {
          val q = new Query("_ah_SESSION")
          q.addFilter("_expires", Query.FilterOperator.LESS_THAN_OR_EQUAL, System.currentTimeMillis)
          DSS.prepare(q).asIterator.foreach(e => {DSS.delete(e.getKey); count = count + 1})
        } finally {
          println(count + " sessions deleted.")
        }
        Full(OkResponse())
      }
    }
    

    Boot.scalaのdef boot内にはこんな感じ。
    LiftRules.statelessDispatchTable.append {
      case Req("cron" :: "sessionCleaner" :: Nil, _, _) => () => SessionCleaner.execute()
    }
    

    ソース全体はgithubに置きました

    2010年3月8日月曜日

    [Lift] GAEがスケールアウトするとLiftセッションが切れる…

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    LiftはServletSessionとは別に独自にセッションを実装しているので、StatefulSnippetはGAEのスケールアウトにうまく対応できないようです…。当然、StatefulSnippetじゃなくてもコールバックをクロージャで実装していると、スケールアウトにタイミング悪くもぶち当たるとアウトなはず。

    以下がスケールアウトしたときのログ。

    ロードバランサがCookie見てセッション同じなら同じサーバインスタンスに振り分けてくれればうれしいのに…。というわけでLift on GAEな人は投票を

    2010年3月5日金曜日

    [Lift] LiftFormを使わずにStatefulSnippetをステートフルに使う

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    LiftFormは大変便利なのですが、Lift流にsubmit時のコールバックをクロージャで書いてしまうと、セッションが切れた時にコールバックが動きません。Google App Engine上で動かしたりなんかすると、3分程度でスピンダウンしてしまうので、すぐ切れてしまってフォーム入力に時間がかかるようなアプリだと、セッション切れてまともに動きません…。(※実際は、ajax_requestが定期的にコールされるので、そんなにスピンダウンしない?と思うけど今作ってるのが携帯向けサイトでajax_requestをOFFにしてるので問題が顕在化)

    そこで、クラシックな通常のHTMLフォームを使い、なおかつStatefulSnippetとして同じインスタンスが呼ばれるようなコードを書いてみました。

    ソースを見る

    まずはLift流に

    普通にLift流に簡単なフォームを書くとこんな感じです。
    <lift:StatefulTest.liftForm form="POST">
      <e:instance/>
      <e:input/>
      <e:submit/>
    </lift:StatefulTest.liftForm>
    

    Snippetはこんな感じ。
    def liftForm(in: NodeSeq): NodeSeq = {
    var name = ""
    def sayHello() = {
      S.notice("Hello, " + name + ". I'm " + this)
      redirectTo("/liftSayHello")
    }
    bind("e", in,
      "instance" -> Text("I'm " + this),
      "input" -> SHtml.text(name, i => name = i),
      "submit" -> SHtml.submit("say hello", sayHello)
    )
    }
    

    無理やり普通のformで

    これと同様なフォームを、無理やり書くとこんな感じです。
    <lift:StatefulTest.myForm>
      
      <form action="mySayHello" method="POST"> 
        <e:instance/>
        <e:key/>
        <e:input/>
        <e:submit/>
      </form>
    </lift:StatefulTest.myForm>
    

    ポイントは、registerThisSnippet()を呼ぶ関数ブロックを、S.fmapFuncで自分で関数マップに登録してやり、そのキーをhiddenで埋め込んでやること(*1)。
    def myForm(in: NodeSeq): NodeSeq = {
    S.fmapFunc((a: List[String]) => {registerThisSnippet()})(key => {
      bind("e", in,
        "key" -> <input type="hidden" name={key} value="_"/>, // *1
        "instance" -> Text("I'm " + this),
        "input" -> <input type="text" name="name"/>,
        "submit" -> <input type="submit" value="say hello"/>
      )
    })
    }
    
    このやり方だと、セッションが続いていれば同じインスタンスのStatefulSnippetが呼ばれ、続いていない場合でも、少なくとも新しいインスタンスのStatefulSnippetで処理は行えます。(CSRFの問題がありますが…)

    ソースはgithubに置きました。http://github.com/pomu0325/lift_sandbox

    2010年2月18日木曜日

    [Scala][GAE] URLFetchのリトライ

    このエントリーをブックマークに追加 このエントリーを含むはてなブックマーク
    Scala勉強会@東北で取り上げていたquerulousRetryingQueryにインスパイアされて、GAEのURLFetchをリトライするコードを書いてみた。

    URLFetchがIOExceptionを投げるか、ステータス500が返ってきた場合にリトライ。
    import com.google.appengine.api.urlfetch._
    
    val UFS = URLFetchServiceFactory.getURLFetchService()
    def retryingURLFetch[A](r: HTTPRequest, retry: Int)(f: HTTPResponse => A): A = {
      try {
        val res = UFS.fetch(r)
        res.getResponseCode match {
          case 500 =>
            if (retry <= 0) f(res)
            else retryingURLFetch(r, retry - 1)(f)
          case _ => f(res)
        }
      } catch {
        case e: java.io.IOException =>
          if (retry <= 0) throw e
          retryingURLFetch(r, retry - 1)(f)
      }
    }
    

    使用例

    import java.net.URL
    import com.google.appengine.api.urlfetch._
    
    retryingURLFetch(new HTTPRequest(new URL("http://pomu0325.blogspot.com")), 2) {
      res => println(new String(res.getContent, "utf-8"))
    }
    
    不安定なAPIを呼ぶ時とかに使えそう。