「AkkaにPull Requestをあげようハッカソン」に参加してきたよ

先日、「AkkaにPull Requestをあげようハッカソン」 に参加してきました。

jsa.connpass.com

akkaコミッターの Konrad さん (https://github.com/ktoso) に加え、 Scala Matsuri 運営の @ さんと @ さん、 さらに会場提供していただいたセプテーニオリジナルの @ さん と @ さん といった Scala 界の豪華メンバーに囲まれて、 Akka の PR を出すために直接相談しならが作業を進めるという、何とも素敵な体験をしましたのでその報告をします。

ちなみに私は時間内に PR を出すことができませんでしが、宿題という形でちゃんと出しました。 敷居は高くないんだよということをお知らせしたいと思います。

私について

  • アーキテクト的な立ち位置で、普段は Javaを書いていてここ最近は C#, Azure
  • Scala はほぼ趣味で Play 書いたり、コップ本、 FP in Scala, Akka in Action 読んだりしました
  • 4月に社内のプロトタイプで好きにやっていいと言われたので、 akka-http の webSocket でチャットボット作ったりしました
  • 英語力はほぼなしで、読みはどうにかなるくらい。
  • OSS の貢献はしたいと思っていたが、結局やってなかった

参加したきっかけ

このイベントの趣旨を見たときは、正直畏れ多いと思いましてためらいましたが、 http://blog.scalamatsuri.org/entry/2017/11/15/133342 などをみて、気軽に行ってみようと思いました。 OSS やりたいけど英語や腕に不安のある私のような人は多いはずですし、良い試金石になるかもという思いもありました。

なお、Scala 大好きな後輩に参加を薦めておいて、私だけ当選しました。ごめんね。

当日について

まず、どの Issue を対応するかについての説明がありました。 https://github.com/akka/akka-http/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 のように、 "help wanted" がついた issue は有志のパッチを受け付けてくれるそうで(知らなかった)、そこから興味があるものを選択して作業し、わからないことは konrad さんや運営スタッフと相談という感じでした。

実装方法の相談、テストの仕方、 PR の相談などを何でも聞いてくれて、 英語のコミュニケーションは @ さんと @ がサポートしてくれて本当に万全の体制でした。ありがとうございます。

他の参加者の @ さん, @ さんがそれぞれ、 alpakka, akka の issue に着手したので、私は経験もあるし、 akka-http の issue をやることにしました。

簡単そうだなーと思ってやってみると、意外に難しいとか、実はもう対応されてたとか色々 issue をあさり、 URLエンコーディングの解析不具合によるエラーがでるという issue を対応することにしました。

github.com

多分簡単だろうと思っていたら、呼び出し場所によって挙動が変わったり、URL解析のための割と複雑なコードがあったりと原因を見つけ、対策を考えるのに割と時間を食い、あえなく時間切れとなりました。 後日絶対 PR 出してやるぞと思いました。

なお、URL解析は結構面倒だし色々な場所で呼ばれるので、ちゃんとパフォーマンスを考えないといけない。 if文一つの追加でもパフォーマンスには影響がでるかもしれないから、 ベンチマークをちゃんとやってから、PRマージするか決めるねという、ありがたいアドバイスをいただきました。

そういう意図なのかわかりませんが、私が直そうとしていたコードは、普通に var や while がありました。 なので、FP 詳しくなくても大丈夫ですよ。

あと、私は Windows マシンで参戦しましたが、開発上は特に問題ありませんでした。 ただし、 PR 出す際に全てのテストを流してわかりましたが、 Windows ではいくつかのテストが失敗またはスキップされます。 なので万全を期すなら Bash on Windows や Docker などでビルドできる環境も用意したほうがいいです。

あと、パエリアおいしかったです。ごちそうさまでした。

PR 出したよ

Adding percent encoding length check for Path.Uri #1553 by kencharos · Pull Request #1590 · akka/akka-http · GitHub

PR 出した後、速攻で Konrad から応答いただけてうれしかったです。

ただ、verup の関係か、修正箇所とは違う場所のCIテストがこけているので、もう少し調べてマージされるまで付き合います。 コア機能に手を入れると色々な箇所に影響を与えそうですね。

そのあと、半日程度で検証・マージされました。 多分ふつうのルートだとこんなに速攻でマージされることはないはずなので、本当にありがたいことです。

思ったこと・感想

前述のブログ記事にもある通り、海外では今回のようなみんなで集まって OSS のパッチを出すイベントが結構あるらしく、それを日本でもという趣旨が素晴らしいと思いました。

OSS への貢献は、特に日本では言語の壁もあるし、コミュニティに参加する人も少ないこともあって敷居が高いと感じていました。初めの一歩が難しいというやつですね。 その初めの一歩を手厚くサポートしてくれるというのは、とてもありがたいことでした。 今日だけでなく、今後もこの活動を続けていきたいと思いました。 何らかの形で、この活動に関わっていければと思います。

あとは英語について。
個人的な話ですが、年、別件で海外のすごいエンジニアの方と会食をするという機会をいただいたことがあったのですが、自分は全く何もできなくて勿体無い思いをしました。
今回も Konrad さんがいらっしゃっていて、Akka だけでなく Scala の動向だとかコミュニティに関する話など色々と直接聞ける機会だったのに、自分は何をやっていたんだろうと。 この業界にいるなら、英語はできないよりできたほうが圧倒的に楽しいし仕事の幅も増えるはずなので、もっと勉強しなければと思いました。努力が必要される場所に身を置きたい。

また、自身の反省として、コントリビューションガイドはちゃんと読んでおくべきだったことが挙げられます。 例えば、"help wanted" という誰でもパッチを上げていいという issue の存在自体をまず知りませんでしたし、 よく読めばどうやって作業したらいいかということが書いてありました。

反省ついでに、akka-http の コントリビューションガイドの抜粋を書いておきます。 PR作るにあたって重要そうな箇所だけを抜き出したので、より詳しく知りたい方は原書を参照してください。

akka-http/CONTRIBUTING.md at master · akka/akka-http · GitHub

Contribution Guideの 抜粋

タグについて

t: で始まるタグはトピックを表し、issue がどの要素に対するものなのかを示します。 t:http:core などです。

すべての issue が開かれていますが、初めての場合は次の2つのタグが付いているものから着手するといいでしょう。

  • help wanted タグはコアメンバーが作業する時間がないか入門にちょうどいいissue です。不明点があれば気軽に訪ねてほしい。

  • nice-to-have (low-priority) は重要度が低いものです。興味があるならやってみてください。コントリビューションを歓迎します。

数字のついたタグは、開発状況を示しています。

  • 0 new は要件が不明瞭なものです。議論をするために、discuss も一緒についていることがあります。内容が変わったりクローズするかもしれません。
  • 1 triage は、有意義なチケットです。パッチを送るなら、このタグがついているものがいいでしょう。
  • 2 pick next は作業対象にしたいチケットで、次回のリリースに含めたいPRに主にコアメンバーが付与します。
  • 3 in progress は誰かが作業中のものです。

他に次のタグが使用されます

bug - バグを示し、feature よりも優先して対応することを示します。 failed - Jeknins の夜間ビルドが失敗したものを示します。

また、 PR の進捗状況のタグは次のように遷移します。

validating => [tested | needs-attention]

PRのワークフロー

| ※ PR を出す前に、6の CLA の登録はしておいたほうがいいです。

  1. 対応するissue を探し、決定する
  2. リポジトリを自分のgithubアカウントにフォークする
  3. git checkout -b wip-custom-headers-akka-http など追加したい featureのブランチを作成して作業する
    • 変更内容の検証をするためには、 sbt の validatePullRequest タスクが役に立ちます。このタスクはmaster ブランチからの変更箇所から影響のあるプロジェクトを特定し、影響のあるプロジェクトすべてのテストを実行します。
      • 例えば akka-http-core のソースを修正すると、akka-http-coreに依存するすべてのプロジェクトでテストが行います。
  4. コミットを以下のルールで作ります
    • Adding compression support for Manifests #22222 のように作業内容の概要とチケット番号を必ず先頭に含めます
      • 簡単なPRならば概要は minor fix などでもOKです
    • 詳細が必要なら1行開けて、詳細な内容を追加します
    • 複数のコミットを行った場合は squashでコミットを1つにまとめてください。
  5. akkaリポジトリにたいして、PR を発行してください。
  6. ここ で CLA(Contributor License Agreement) に登録してください。
    • githubアカウントが、CLA に登録されていない場合、 PR の検証が OK になりませんので、事前に登録しておいた方がよいです。
  7. 初めてのPRの場合など、アカウントがホワイトリストに登録されていない場合、 BotCan one of the repo owners verify this patch? とたずねてきます。コアメンバーが確認をして、OKを回答します。これは悪意のあるPRによって、ビルドが壊れることを避けるためです。
  8. アメンバーや興味のある人によってレビューや場合に応じて質問や修正依頼が来ます。質問などは気軽にしてください。通常 2つの LGTM が得られたらマージする対象として認められたことになります。
  9. 次回のリリースで、PRが本体にマージされます!

最後に

とても貴重な体験をしました。ありがとうございました!

jBatch RI を Java SE でまともに(JPA,CDI,Transactionalつきで)動かす #javaee

この記事は、Java EE Advent Calendar 4日目の記事です。

昨日は @lbtc_xxx さんの Java EE ブログエンジン Apache Roller のご紹介でした。

明日は、making@github さんです。

概要

背景を説明すると、既存の Java 製のオレオレバッチフレームワークがあるんだけど、 設計が古いので、改善したいと。

標準の jBatch(JSR-352)ってのがあるけど、これアプリケーションサーバーいるんでしょ? ちょっと面倒だなーとかと思ってたら、 Java SE 環境でも動くらしい。

yoshio3.com

が、色々と足りないというか、Java EE 向けに書いたソースが、以下のようにそのままでは使えないこともわかりました。

  • CDI が使えない
    • ジョブXML の ref 属性に、 @Named の名称で指定できない
  • Batchlet で @Transactional が使えない
    • JTA 入ってないしね
  • チャンク方式における分割コミットが動作しない
  • JPA も使いたいよね

というわけで、この辺を改善して、SE環境で使う際の制限を取っ払うことができないかというお話です。

内容的に、Java EE でいんだろうかと思わないわけでもないですが、タイミングがよかったのと、色々と知見を得たのでまとめてみたいと思いました。

最終的なソース一式は Github にあります。

前提として、CDI, JPA, jBatch を一通り触ったことがあるくらい人向けの内容です。 知らない人でもそれぞれの入門記事を見てもらえればわかるんじゃないかと思います。

環境準備

上で引用した記事ですと、jBatch の参照実装と derby なんかのjarファイルを取得してクラスパスに設定するようになっていましたが、 調べると Maven リポジトリがあったので、ちゃんと依存性を管理するようします。

やりことを全部含めた 依存性定義はこんな感じになりました。

      <!-- Java EE API もろもろ -->
      <dependency>
         <groupId>javax</groupId>
         <artifactId>javaee-api</artifactId>
         <version>7.0</version>
      </dependency>
      <!-- glassfish に載ってる jBatch の参照実装 -->
      <dependency>
         <groupId>com.ibm.jbatch</groupId>
         <artifactId>com.ibm.jbatch-runtime</artifactId>
         <version>1.0</version>
      </dependency>
      <!-- jBatch RI がジョブ実行状況の記録に使うDB -->
      <dependency>
         <groupId>org.apache.derby</groupId>
         <artifactId>derby</artifactId>
         <version>10.12.1.1</version>
      </dependency>
      <!-- CDI 実装 -->
      <dependency>
         <groupId>org.jboss.weld.se</groupId>
         <artifactId>weld-se</artifactId>
         <version>2.3.1.Final</version>
      </dependency>
      <!-- JPA 実装 -->
      <dependency>
         <groupId>org.eclipse.persistence</groupId>
         <artifactId>org.eclipse.persistence.jpa</artifactId>
         <version>2.5.2</version>
      </dependency>
      <!-- お好きなJDBCドライバ -->
      <dependency>
         <groupId>org.postgresql</groupId>
         <artifactId>postgresql</artifactId>
         <version>9.3-1102-jdbc41</version>
      </dependency>

jBatch RI の概要

  • ジョブのステータス管理として Derby を使っている
  • ジョブの実行はConcurrency Utility のスレッドプールで動いている
    • このスレッドプールは、Java SE 環境でも有効なため、ジョブの再投入や並列実行もOK
    • 既存バッチは、1ジョブごとにJVMを起動するので起動時間がネックとなっていたため、これは大きな利点
  • 設定やカスタマイズは、 META-INF/servicesの下に batch-configやbatch-services というプロパティファイルで行う。
    • 前者はジョブ管理用の DerbyのURLとかを設定
    • 後者は、カスタマイズ用の定義ファイルで、jBatch RI はサービスという形式で拡張ポイントを用意している
      • 今回は、CDIを使ってジョブインスタンスを取ってきたり、チャンクトランザクション制御のカスタマイズとかに使っている
      • プロパティのキーにサービス名、値に所定のクラスを継承して作ったクラスの FQDNを書いておく

Batchlet でJPAと @Transactional を使う

さて、では実際にどのような拡張を行ったかを紹介します。

まずは今回動かす Batchlet と job xml です。

Batchlet は JPA でテーブルのデータを消すものです。

package batchlet;

import javax.batch.api.AbstractBatchlet;
import javax.batch.api.BatchProperty;
import javax.batch.runtime.context.JobContext;\
import javax.enterprise.context.Dependent;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;

@Dependent
@Named // これで、"dataDeleteProcess" だけで、JOB XML に記述できる。 
@Transactional
public class DataDeleteProcess extends AbstractBatchlet{

    @Inject
    private JobContext ctx;

    @Inject // プロデューサーメソッド経由で取得
    private EntityManager em;
    
    @Override
    public String process() throws Exception {
        
        int deleted = em.createQuery("delete from Employee e").executeUpdate();
        
        System.out.println(deleted + " rows deleted.");
       
        
        // 終了ステータス
        return "COMPLETE";
    }
}

次は job xml。 Batchlet に @Named アノテーションが付いているので、ref 属性は CDI の bean 名称で指定可能です。

<job id="cleanup-job"
     xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
     version="1.0">
      
  <step id="clean">
    <batchlet ref="dataDeleteProcess">
        
      <properties>
        <property name="text" value="TEXT"/>
      </properties>
    </batchlet>
  </step>
</job>

ついでに、 EntityManger を @Inject できるようにするプロデューサーフィールド

@Dependent
public class EMProducer {
    @PersistenceContext(unitName = "default")
    @Produces
    private EntityManager em;
    
}

これは、 Java EE 環境では普通に動きますが、SE環境では、以下の理由で動きません。

  • @PersitenceContext は Java EE環境のみ(だと思う) なので、対応できない
  • JTA が無いので、@Transactional が動かない
  • CDI も有効でないので、ref属性にはクラスの FQDN を書かなくてはいけない

というわけで1つ1つ解消していきます。

JPAJava SE環境で使う

JPAJava SE 環境で動かすには、JTA ではなくローカルトランザクションで動かします。 (JTA 詳しくないです。 SE で動かすことできるの?)

というわけで、 persistence.xml を書き直し、データソースではなく JDBC経由とする。

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
  <persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
        <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver"/>
        <property name="javax.persistence.jdbc.url"
             value="jdbc:postgresql://localhost:5432/postgres"/> <!--好きなDBを設定 -->
        <property name="javax.persistence.jdbc.user" value="postgres"/>
        <property name="javax.persistence.jdbc.password" value="postgres"/>
      <property name="eclipselink.logging.level" value="FINE"/>
      <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
    </properties>
  </persistence-unit>
</persistence>

で、 EntityManger をインジェクションするためのプロデューサーメソッドも書き直す。

package db;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.Produces;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

@Dependent
public class EMProducer {
    
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("default");
    
    @Inject
    private EMHolder holder;
    
    @Produces
    public EntityManager getEm() {
        // ホルダーにEMが無い場合やあるけどトランザクションが終了している場合(※1)は新規取得
        // ※1 ジョブの複数起動でスレッドが再利用された場合
        if(holder.getEM()== null || !holder.getEM().isOpen()) {
            EntityManager em = emf.createEntityManager();
            holder.setEM(em);
        }
        
        EntityManager em = holder.getEM();
        
        return em;
    }
}

ここに出てくる EMHolder とはこんなの

/**
 * スレッドローカルでEntityMangerを保持するBean
 */
@ApplicationScoped
public class EMHolder {

    private ThreadLocal<EntityManager> holder = new ThreadLocal<>();
    
    public void setEM(EntityManager em) {
        holder.set(em);
    }
    
    public EntityManager getEM() {
        return holder.get();
    }
    
    public void removeEM() {
        holder.remove();
    }
}

これは何をやっているかというと、スレッド単位で EntityManger(=コネクション) を1つだけ生成したいために、 スレッドローカルをアプリケーションスコープで作っている。

jBatch で CDI を使うと、 Bean は全て @Dependent なのだが、Java SE環境では、@Dependent は都度インスタンスを生成してしまう。 こうすると、EntityManger のインジェクションごとに、EntityManger を生成していまうので、 複数 bean 間で EntityManger の共有ができないため、スレッドローカルを用いている。

@Transactional に反応するインターセプタを作る

JTA を使わないので、@Transactional によるトランザクション制御は自前のインターセプタを作って行う。

package se;

import db.EMHolder;
import javax.inject.Inject;
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
import javax.interceptor.InvocationContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.transaction.Transactional;

@Transactional
@Interceptor
public class SeTransactionInterceptor {

    @Inject
    private EntityManager em;
    
    @Inject
    private EMHolder holder;

    @AroundInvoke
    public Object doTransaction(InvocationContext ic) throws Exception {
        EntityTransaction tx = em.getTransaction();
        Object result = null;
        try {
            if (!tx.isActive()) {
                tx.begin();
            }
            
            System.out.println("tx interceptor start");
            result = ic.proceed();
            if (tx.isActive()) {
                tx.commit();
            } 
        } catch (Exception e) {
            try {
                if (tx.isActive()) {
                    tx.rollback();
                }
            } catch (Exception e2) {
            }
            throw e;
        } finally{
            holder.removeEM();
            if(em.isOpen()) {
                em.close();
            }
        }
        return result;
    }
}

やっていることは、メソッド実行前にトランザクションを開始して、成功すればコミット、例外が起きればロールバックというもの。 本当だと、例外の種類によってはコミットしたり、トランザクション属性なんかもあるけど、とりあえず割愛。 ここで、インターセプタとBatchlet で EntityManger を共有するためにも、前述の EMHolder が生きてくる。

DB周りはこれでOKなので、次は CDI

CDI を有効にする

上で述べたとおり、サービスによってjBatch RI は拡張可能になっている。 JOB XML から、 ジョブインスタンスを取得する箇所は、 batch-services.properties に、 CONTAINER_ARTIFACT_FACTORY_SERVICE を設定すると拡張できる。

また、jBatch RI は、 Weld SE(Weld は glassFishでも使っている CDI 実装。SEはそのJava SE版) を使った 上記のサービス実装が存在しているため、

CONTAINER_ARTIFACT_FACTORY_SERVICE=com.ibm.jbatch.container.services.impl.WeldSEBatchArtifactFactoryImpl

のように書けば、CDI が使用できるし、ref属性に Bean の名前が書けるようになる。

よかったこれで解決ですね。と思ったのだけど、ジョブを2回投入すると2回目のジョブが起動できない。。。

WeldSEBatchArtifactFactoryImpl をみていると、ジョブインスタンスを取得するたびに、 Weldコンテナ(DIコンテナ)を生成しているため、 この辺に原因が有りそうだと踏んだ。

実際、このソースを参考に、Weldコンテナを1回だけ生成するようなサービスを作ったら、この問題は解消した。

package se;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Named;

import org.jboss.weld.environment.se.WeldContainer;

import com.ibm.jbatch.container.exception.BatchContainerServiceException;
import com.ibm.jbatch.spi.services.IBatchArtifactFactory;
import com.ibm.jbatch.spi.services.IBatchConfig;
import org.jboss.weld.environment.se.Weld;

@Named("MyWeldBean")
public class WeldContainerFactory implements IBatchArtifactFactory {

    // 1回だけ生成
    WeldContainer weld = new Weld().initialize();
    @Override
    public Object load(String batchId) {
        Object loadedArtifact = getArtifactById(batchId);

        if (loadedArtifact == null) {
            return loadedArtifact;
        }

        return loadedArtifact;
    }

    private Object getArtifactById(String id) {
        Object artifactInstance = null;

        try {
            // 元の実装はここで毎回 Weld().initialize();
            BeanManager bm = weld.getBeanManager();

            Bean bean = bm.getBeans(id).iterator().next();

            Class clazz = bean.getBeanClass();

            artifactInstance = bm.getReference(bean, clazz, bm.createCreationalContext(bean));
        } catch (Exception e) {
        }
        return artifactInstance;
        
    }
//(割愛)
}

このサービスを有効にするには、batch-sevices.properties にこう書く。

CONTAINER_ARTIFACT_FACTORY_SERVICE=se.WeldContainerFactory

これで、Java SE 環境でも Java EE の要領で記述した batchlet が使えるようになった。

チャンク形式のトランザクション制御を実現する

jBatch のチャンク形式は、大量データの処理に向いていて、job xml のパラメータ設定だけで、 データのコミットを行う間隔を変更できたりします。

例えば、以下は読込んだデータについて2件ごとにコミットする設定になります。

<?xml version="1.0" encoding="UTF-8"?>
<job id="chunk-sample" xmlns="http://xmlns.jcp.org/xml/ns/javaee"  version="1.0">
    <step id="insert">
        <chunk item-count="2"><!-- item-count がコミットする件数 -->
            <reader    ref="employeeFileReader">
                <properties>
                  <property name="input" value="c:/temp/input.txt"/>
                </properties>
            </reader>
            <processor ref="employeeProcessor"/>
            <writer    ref="employeeWriter"/>
        </chunk>
    </step>
</job>

ですが、これも Java EE の場合のみです。 jBatch RI は Java SE環境だと、トランザクション制御を行ってくれません。 といわけでこれも対応させてみます。

トランザクションサービスの作成

トランザクション制御に関する処理も、TRANSACTION_SERVICE というサービスで拡張可能になっています。

ここでは、既存のトランザクション制御用のサービスを上書きし、EntityManger のトランザクションを利用してトランザクション制御を行うように自作サービスを作成します。

package se;

import db.EMProducer;
import com.ibm.jbatch.container.exception.TransactionManagementException;
import com.ibm.jbatch.container.services.impl.BatchTransactionServiceImpl;
import com.ibm.jbatch.spi.services.TransactionManagerAdapter;
import javax.batch.runtime.context.StepContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.inject.spi.CDI;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;

public class SeTransactionManagerImpl extends BatchTransactionServiceImpl {

    @Override
    public TransactionManagerAdapter getTransactionManager(StepContext stepContext) throws TransactionManagementException {

        return new TransactionManagerAdapter() {
            EntityTransaction tx;

            @Override
            public void begin() {

                BeanManager beanManager = CDI.current().getBeanManager();
                Bean<?> empBean = beanManager.getBeans(EMProducer.class).iterator().next();

                EMProducer emp = (EMProducer) beanManager.getReference(empBean, 
                        EMProducer.class, beanManager.createCreationalContext(empBean));
                EntityManager em = emp.getEm();
                tx = em.getTransaction();
                tx.begin();
            }
            @Override
            public void rollback() {
                tx.rollback();
            }
            @Override
            public void setRollbackOnly() {
                tx.setRollbackOnly();
            }
            @Override
            public void commit() {
                System.out.println("tx commit");
                tx.commit();
            }
            @Override
            public int getStatus() {
                return 0;
            }
            @Override
            public void setTransactionTimeout(int arg0) {
            }
        };
    }
}

TransactionManagerAdapter というのが、トランザクション制御用のインターフェースです。 ここではかなり無理やりですが、 CDI の BeanManger から Batchlet の項で作成した EMProducer を取得して、EntityManger を取得し、 トランザクションを取得しています。

今のところ、JPA に限定した実装ですが、JDBCコネクションにすることもできるでしょう。 また、begin,commit,rollback以外の実装は適当です。

これで、チャンク形式の機能も Java EE 同様に近づいたと思います。

並列処理などの検証はしていないですけど、多分動きそう。

(おまけ)ジョブ投入方法

mainメソッドなどから、ジョブを投入するには、以下のようなコードを書けばOKです。

    private static void executeJob(String jobId) {
        long execID=0;
        // ジョブの開始時はXML読み込みなど同時実行されるとおかしくなる処理があるので、ジョブID単位で同期化しておく。
        JobOperator jobOperator = BatchRuntime.getJobOperator();
        synchronized(jobId){
            Properties props = new Properties();
            execID = jobOperator.start(jobId, props);
            System.out.println("job stated id:" + execID);
        }
        JobExecution jobExec = null;
        // ジョブが終了するまでポーリング
        while (true) {
            jobExec = jobOperator.getJobExecution(execID);
           
            if (jobExec.getEndTime() != null) {
                break;
            }

            try {
                Thread.sleep(1000L);
            } catch (InterruptedException ex) {
            }
        }
        System.out.println("JOB END:Status is " + jobExec.getExitStatus() );
        
    }

jobOperator.start はジョブを開始すると、ジョブの実行終了を待たずにジョブIDを返して終了します。 そのためジョブの終了を待つためには、ポーリングするなどの対処が必要になります。 ここでは、ジョブ投入ごとにポーリングしていますが、ジョブ終了を検知するだけのタスクを別に作るという方向性でもいいと思います。 正直この辺は面倒ですね。標準でジョブ終了まで待つような仕組みが欲しいです。

まとめ

というわけで、 jBatch を拡張して Java SE でもそれなりに使うという話でした。

Java EE は色々と最初から用意されているので、楽だとあらためて思いました。

また、Java SE で使わなくても、 ジョブ単位でユニットテストをしたいという場合には、使える内容なんじゃないかと思います。 あと、CDI の裏側の仕組みも色々知れたので個人的には楽しかったです。

今回色々やってみて、スレッドプールでジョブが実行されるのがわかったのは、JVMの起動回数を抑えることができそうなので、結構有用でした。 とはいっても、 このプログラムを常駐化させてジョブ投入のリクエストを受け付けるようにするのは、 ソケット使ったりする必要があるのでわりと面倒ですけどね。

本格的にやるなら、 paraya micro で埋め込みサーバ使ったり、 あるいは Spring boot + Spring batch(+ JSR-352 Support) で組んでしまった方が楽かもしれません。

Javaで静的リソースを返すだけのHTTPサーバーをやっつけで

信頼性は無くてもよいので、複数のディレクトリにあるファイルをhttpで提供する必要が出てきたのでやっつけで作った。

javaによる複数の特定ディレクトリ以下の静的リソースを返すHttpサーバー(やっつけ実装)

色々と制約があり、jettyのような外部ライブラリを使わないでおこうと思ったので、 com.sun.net.httpserver.HttpServer というJDKに付属している非公開のAPIを使用している。 com.sun.net.httpserver.HttpServer はシンプルで良いのだが、特定URL配下に特定のディレクトリを割り当てるようなAPIが無かったので、 NIO2を使用してディレクトリ配下を走査しつつURLを割り当てている。

gitbucketのActiveDirectory認証の設定例

gitbucket(takezoe/gitbucket · GitHub)

がお手軽にgithub cloneを構築できるのですごく良いです。

社内のActiveDirectoryと連携できれば便利なので、以下を参考に色々やってました。

https://github.com/takezoe/gitbucket/wiki/LDAP-Authentication-Settings

GitBucketのユーザ認証をActiveDirectoryと連携する - C Sharpens you up

が、上手く行きません。以下のように認証エラーが出ます。

[LDAP: error code 49 - 80090308: LdapErr: DSID-0C0903A9, comment: AcceptSecurityContext error, data 773, v1db1 ]

ActiveDirectoryの知識が全く無いので、色々な構成があるのかもしれませんが、

結局、以下のような設定で上手く行ったので設定例を共有しておきます。 似たような事例で困っている人がいたら参考にしてください。

私の環境での設定例

  • LDAP Host : (社内のActiveDorectoryホスト)
  • LDAP Port : 389
  • Bind DN : user@domain.local (FQDNでユーザーアカウントのみを設定。cn=等は不要)
  • Bind Password : 上記ユーザーのパスワード
  • Base DN : OU=hoge,DC=domain,DC=local(環境毎に異なる)
  • User name attribute : sAMAccountName
  • Full name attribute : displayName
  • Mail address attribute : mail

上記の設定で、社内ActiveDirectoryと連携し、アカウントのみでログイン可能となりました。 また、メールアドレス、フルネームも問題なく連携されました。

Rのインストール、RODBCでDBテーブルのDELTE,INSERTを行う。

Rはじめました。
統計解析やるぞーって段階ではなく、Rの実行環境を作ってる最中です。
データベースとの連携で色々面倒だったのでメモ。

Linux(Cent OS)への導入

(最新だとわかりませんが)CentOS 6.4には、yumにRはありませんのでEPEL導入します。
導入後もRのライブラリインストールでこけたりするので、どちらにしろEPELは入れたほうがいい。 sudo rpm -Uvh http://ftp-srv2.kddilabs.jp/Linux/distributions/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
sudo yum install R
あと、必要ならR Studio Server入れます。 RStudio
R Studio ServerすごいよR Studio Server。

RODBC, XMLを使うならこれらのライブラリから参照されるヘッダファイルが必要なのでインストール
sudo yum install unixODBC unixODBC-devel libxml2-devel
今後も必要に応じて追加していく。

あとは、DB用意したり、ODBCドライバ用意したり、 /etc/odbc.ini を書いたりする。

RODBCの利用

Rを起動し、

    #パッケージインストール,初回のみ
    install.packages("RODBC")
    library(RODBC)
    # ODBC名を引数に渡す。ユーザー、パスワードが必要なら引数を追加。
    conn <-odbcConnect("myodbc")
    # テーブルの全データ取得
    data <- sqlFetch(conn, "Table")
    # 適当にデータ加工
    
    # テーブルにデータ反映。第三引数がないと、第二引数の変数名(この場合data)でテーブルをcreateする。
    sqlSave(conn, data , "Table2")

SQL書かなくてもDBへデータ反映できるのはすばらしい!
のだけど、このsqlSave() デフォルトの動作は指定されたテーブルをcreateする。
(既存テーブルがある場合は実行不可で、 実行前に sqlDrop(conn, "Table2") を実行する。)
Delete&Insertでどうにかできないものか。

エラー

sqlSave()の引数を見てみると、saferオプションがある。TRUEだとCleate, FALSEだとテーブルがなければcreate,あればDELETE&INSERTを行うとあるので、これで解決
sqlSave(conn, data, "Table", safer=FALSE)
と思いきや、こんなエラー

Query: INSERT INTO `Member` ( `ID`, `NAME`, `AGE`, `GENDER` ) VALUES ( ?,?,?,?,? )
Binding: 'ID' DataType 4, ColSize 10
Binding: 'NAME' DataType -9, ColSize 120
Binding: 'AGE' DataType 4, ColSize 10
Binding: 'GENDER' DataType -8, ColSize 1
Parameters:
no: 1: ID 1/***/no: 2: NAME test/***/no: 3: AGE 32/***/no: 4: GENDER 0/***/
 *** caught segfault ***
address 0x2028b3780, cause 'memory not mapped'

Traceback:
 1: .Call(C_RODBCUpdate, attr(channel, "handle_ptr"), as.character(query),     data, ds - 1L, params, as.integer(vflag))
 2: odbcUpdate(channel, query, mydata, coldata[m, ], test = test,     verbose = verbose, nastring = nastring)
 3: sqlwrite(channel, tablename, dat, verbose = verbose, fast = fast,     test = test, nastring = nastring)
 4: sqlSave(conn, data, "Member", verbose = TRUE, safer = FALSE)

SEGVやめて、、、。ちなみにWin版だとこんなログ。

 odbcUpdate(channel, query, mydata, coldata[m, ], test = test,  : 
  missing columns in 'data'

解決

何か、列数とパラメータ数あってないぞ。

    Query: INSERT INTO `Member` ( `ID`, `NAME`, `AGE`, `GENDER` ) VALUES ( ?,?,?,?,? )

というわけで、よく見てみると、通常のsqlSaveでテーブルcreateした場合、一番最初にrownamesという列が勝手に追加されている。
設定されているのはデータフレームの行番号みたいで、これはデータフレームをcsvとかに出力した場合でも付与されるので、よくある話なのかな?
というわけで、解決方法は以下の通り。

sqlSave(conn, data, "Table", safer=FALSE, rownames=FALSE) 引数 rownames=FALSE を追加すれば,rownames列は設定されず、データフレーム内のデータのみでINSERTを行ってくれる。

ちなみに、ODBCドライバのせいかはよくわからないが、テーブル名やカラム名の大文字、小文字の違いよってもエラーが出たりするので、解決しない場合はDB側にも調査が必要。

SIerから転職します

9月末をもって現職のSIerから転職する事になりました。
転職先は今流行りのWeb系ではなく、ITコンサルとかSIとかをやる会社で、10月より働き始めます。
僕には華々しい実績があるわけではありませんが、 現職のこと、転職のことなんかを簡単にまとめ、誰かの参考になったら幸いだし、 数年後自分でこのエントリを見て気持ちの変化が無いかを確認してみたいと思う。

おおざっぱにまとめると

転職の理由を一言で言えば、"俺10年くらい仕事でJavaしかやってないけど、このままだと10年後もJavaやってんじゃないの?" という思いに尽きる。
(Javaをdisるわけではなく、それしか選択肢が無いという状況をどうにかしたい)
別に仕事があるならそれでいいじゃんという人もいると思うが、個人的にはエンジニアとしてこれからも生きていくならもっと面白いと思うことをやって行きたいと思った次第。

あんた誰?

都内の某中堅独立系SIerに9年ほど勤務したことになった。
金融、流通、通信等に主な顧客を持ち、客先常駐にてSIまたは運用を行うよくあるSIerだと思う。 社名をgoogle先生に尋ねれば、ブラックが関連検索に出てくるけど、別にブラックだと思った事はない。上には上が、下には下がいるだけのことで、仕事を仕事と割り切れば普通に働けるいい会社だと思う。
私自身は金融部門のSEとして、信販とか銀行だとかを相手にSEとして仕事をやっていた。 プログラム、設計、テスト、お客様との仕様に関する打ち合わせなど一通りの経験は積ませてもらい、 一応リーダーなんかもやっている。
割と環境に恵まれた感はあり、色々と責任ある仕事は任せてもらえたし、毎日終電なんて言う事はほとんどなかった。
ただし、金融のシステムは規模が大きく、複数のITベンダー入り交じりで2,3年かけて開発を行う事がザラである。 また技術も主体的に選ぶ事ができなくて、大体は頭にNとかIとかHとかがつく会社がサーバー、言語、フレームワーク、ミドルウェアを決めてしまっていることがほとんどだ。
そこから仕様を決めて開発をやっていくのも面白かったんだけど、

  • ちゃんとユニットテスト作ってJenkinsでCI回して楽しようよ
  • RedmineとかTrac導入してエクセルで課題管理やめようよ
  • エクセル、ワード以外にもっといい設計書のフォーマットってあるんじゃないの?
  • RubyとかScalaとかやろうよ

みたいな、技術策定、プロセス改善みたいなことって今の委託の立場だとできないなーとここ数年くすぶっていたし、暇がある時に興味があることについて調べたり勉強会行ってみたり、自宅でコード書いたりするようになった。

転職について

というわけで、上記のくすぶりが転職する理由となった。
(現職に思う所が全く無かったわけでないけれども) 転職先の候補としては、エンジニアとして経験を積めることが第一で

  • 技術志向で主体的にSI,IT導入ができるSIer,コンサル
  • 内製でITサービスを提供している事業会社

に絞った。プレゼン作成とかベンダコントロールに明け暮れるような仕事はしたくなかった。どんなポジションになったとしてもコード書いたり、設計したりと手を動かす余地は残していたかったから。
前者、後者とも複数の企業に応募し、前者に属する、とある企業から内定を頂き社員の方に刺激を受けた事もあり、良い経験ができると思い、転職することになった。
後者に属する企業にも興味はあったし内定を頂いた会社もあったのが、その企業が提供しているサービスや事業領域に強い興味と知識が無いと、結局の所サービスの改善とかに結びつくようなものを作れないんじゃないかと思い、今は腕を磨きつつ、これは面白いと思える物が見つかったときにあらためて考えてみようと判断した。(あと単純に、金融系の経験だとスキルマッチしにくいという点もあったんだけど)

転職活動

リクナビ、転職エージェント、greenなんかは一通りやった。 勉強したせいかのアウトプットのため、ブログ作ったりgithubでアプリ公開とかをやったりした。
転職の決め手となったのはCodeIQで、ウチ来ない問題に応募した所、転職先の企業からオファーをもらった。
自分の書いたコードを見てもらっているので、企業側は履歴書から判断できない実スキルを知る事ができるし、応募者側も自分の力量を認めてもらえた感があるので双方前向きな気持ちで選考できたんじゃないかと思う。
というわけで、CodeIqに参加している会社で、興味のある会社がにあるんだったらじゃんじゃんコード書いて応募すればいいと思うし、企業さん側もCodeIQに参加してみてはと思う。

まとめ

僕が就職した10年前ってこれからはJavaの時代、しかもJ2EEはすごいぜみたいな感じだったと記憶しています。 それが現在では、Java自体もSpringからEODみたいな考え方が生まれて開発しやい方向に進化したし、RoRみたいなLL言語フルスタックフレームワークが台頭したし、WebはAJAXから始まるUI周りの進化が著しいし、iPhone,Andoroidの誕生、基盤系ではクラウドが当たり前みたいな感じで、この10年間だけでも本当に色々なことがあったなと思うし、これらについて今の職場で触れる機会があんまりないことに惜しいなと思っていた。(金融系の基幹業務は、こういう流行が一番最後に来る場所だと思っている)
今のIT界隈は、オープンな技術要素やクラウド基盤を組み合わせる事で、アイデアを素早く実現したり、今まで色々な制約があってできなかった事ができてしまったりと、色々と面白い仕事ができそうなんじゃないかと個人的に思っている。
別に転職先が自分の夢が全て叶う場所だとは思っていないけど、今より自分のやりたい事ができるのは確実だと思うので、期待と不安を混じらせつつ次の一歩を踏み出します。

ConcurrentHashMapを使うならatomicなメソッドを使おう

ConcurrentHashMapを使うときの注意点

タイトルの通りです。

ConcurrentHashMapは同期を取りつつ、パフォーマンスも優れたとても優秀なやつで、最近Webアプリケーションで、キャッシュみたいなものを作るときに使いました。

とはいえ同期を取るとはいっても以下のようなキーの存在を確認して、putするような複合アクションの呼び出し(いわゆるcheck-then-act)では同期されません。(別にこれは、従来のHahMapとかCollections.syncronizedMapとかでも同じです)

  // atomicではない操作。
    if(!map.containsKey(key)) {
        map.put(key, value);
    }

以下が実証コードで、実行すると高い確率で複数スレッドで同一キーへのputを行います。
(今回のケースだとあまり問題にならないですが、valueを更新するような操作だと更新内容が喪失する可能性はあります。
また同期化されない処理は後々厄介なバグを生んだりするそうです。参考資料より。)

Atomicな操作

で、こんな時のためにConcurrentHashMapには、キーがなければ値を追加するという上記のような操作を行うputIfAbsentメソッドがあります。

  // 上とほぼ同等な内容だが、atomicな操作。
    map.putIfAbsent(key, value)

atomicなメソッドはほかに、replace, removeなんかがあります。 というわけで、もし既存のソースでMapをConcurrentHashMapに置き換える時は、上記のようは複合アクションを行っていないかも確認した方が良いという話でした。

参考資料

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―