AWS CodeBuildでいつの間にかDependency Cacheがサポートされてた

re:Inventの流れが早くて見逃していたのですが、AWS CodeBuildで依存関係のキャッシュ機構が使えるようになっていました。

aws.amazon.com

昔、自前で似たようなことをするためのやり方を試行錯誤してたのですが、もう不要ですね。

jappy.hatenablog.com

あとあるとすれば、ビルドにカスタムDockerイメージを使っている場合に、イメージのダウンロードに時間がかかってしまう課題が解消できると、最強な気がします。

SOAP API(wsc)で地理位置情報型や時間型を扱う #salesforce

Advent Calendarの季節になりましたね。世のエンジニアの1年間のノウハウが放出されるイベント、楽しみです。

さてそんな中、Advent Calendarとまったく関係のない記事になりますw(エントリを逃したとも言う…)

SFDC謹製のJavaAPIクライアントであるWSC (Force.com Web Service Connector)を使って、ちょっとクセのある(?)「地理地情報型(Location)」や、Summer'17でパイロットリリースされた「時間データ型(Time)」を操作する時のやり方についてのメモです。

地理位置情報型・時間型の扱い方

先に結論ですが、それぞれ以下のように扱うことで、利用できます。

  • 地理位置情報型
    • 緯度と経度を別々に扱えばOK。例えば項目のAPI参照名が FooLocation__c の場合は、 緯度が FooLocation__Latitude__s, 経度が FooLocation__Longitude__s のようになる
  • 時間型
    • "00:00:00.000Z" の形式

地理位置情報型、以下のIssueにある通り、少し前まではダウンロードしてきたWSDLに手を加えないと使えなかったのですが、今は直っているみたいですね*1

github.com

また、時間データ型についてはベータなので、今後変更になる可能性もある気もしますが、現時点での結果ということで…。

サンプルコード

レコード1件をINSERTする場合の例。

public class Sample {

    private static final Logger log = LoggerFactory.getLogger(Sample.class);

    public static void main(String... args) throws Exception {
        ConnectorConfig config = new ConnectorConfig();
        config.setUsername("[ログイン名]");
        config.setPassword("[パスワード]");

        PartnerConnection connection = Connector.newConnection(config);
        
        SObject obj = new SObject("Hoge__c");
        obj.setField("Name", "test");
        obj.setField("Time1__c", "12:00:00.000Z"); // これで 12:00 として時間が登録される
        obj.setField("Location1__Latitude__s", "35.1");
        obj.setField("Location1__Longitude__s", "135.2");

        SaveResult[] results = connection.create(new SObject[]{obj});

        for (SaveResult result : results) {
            log.info("{} {} {}", result.getId(), result.getSuccess(), result.getErrors());
        }
    }
}

実行環境

  • Java 8
  • WSC : "42.0.0" タグ
  • Partner WSDL : API Version 41.0

*1:しれっと会社のGitHubアカウントで +1 していますが。

Java8のLambda式とGroovy

Spockでテストコードを書く時の備忘録。

Consumer : void accept(T)

メソッド

public void method1(List<String> list, Consumer<List<String>> consumer)

Java

hoge.method1(foo, list -> list.add("aaa"));

Groovy

hoge.method1(foo, { list -> list.add('aaa') })

Predicate : boolean test(T)

メソッド

public void method2(String text, Predicate<String> predicate)

Java

hoge.method2("test", s -> s.length() < 5);

Groovy

hoge.method2('test', { s -> s.length() < 5 }) // size() でもOK

Supplier : T get()

メソッド

public void method3(Supplier<String> supplier)

Java

hoge.method3(() -> "test");

Groovy

hoge.method3({ 'aaa' }) // -> 'aaa'
hoge.method3({ -> 'bbb' }) // -> 'bbb'

Function : R apply(T t)

メソッド

public void method4(String text, Function<String, String> function)

Java

hoge.method4("abcde", s -> s.toUpperCase()); // -> "ABCDE"
hoge.method4("abcde", String::toUpperCase); // -> "ABCDE"

Groovy

 hoge.method4('abcde', { s -> s.toUpperCase() }) // -> 'ABCDE'

ちなみに、Groovy2.6からは、オプションとして -Dgroovy.antlr4=true を指定すると、GroovyでもLambda式がそのまま使えるみたいです(このブログを書いた直後に偶然知りました)

Trailheadはじめました(2) #salesforce

10月も終わろうとしていますが、依然とほぼ同じペースで時間を確保することができ、大分進みました。

  • ランク: Ranger
  • バッジ: 202(内1つはハロウィーンのイベントバッヂ)
  • ポイント: 165,325
  • トレイル: 32

50個ごとにSuperbadgeにチャレンジするという計画は、100個取った時点で軌道修正しました。
そもそも今ある全部のバッヂを足しても300行かなそうなので…。というわけで、週末を利用してSuperbadgeはコンプリートしました。

必然的に日本語訳されていないモジュールに取り組むことも多くなりましたが、英語と日本語で内容が違っているのとかは相変わらずでしたね。
特に、Lightningコンポーネント関連のモジュールは多かった気がします。それだけ発展途上ってことなのかなと思いました。

今後は、ちょっとペースを落としつつ、お気に入りしているボリュームのあるモジュール(とプロジェクト)を消化していこうかなぁと思います。

Trailheadはじめました #salesforce

ずっと食わず嫌いで手を付けていなかった、というわけでもないのですが、ドラクエ11も裏ボスまで倒してやることがほぼなくなったということもあり、Trailheadをはじめました。

やってみると、結構楽しいw

今のところのスコアはこんな感じ。

  • ランク: Expeditioner
  • バッジ: 54
  • ポイント: 44,650
  • トレイル: 3

トレイルが少ないのは、手を付けやすそうなモジュールからつまみ食いをしていたためだと思われます。

もうちょっと体系立てて選んでいけばよかったなと反省。。

すすめ方

勉強時間は、平日は22時~1時くらい。休日は日によってまちまちですが、多くても6時間くらい。(大体ドラクエ11をやっていた時間ですね
飽きっぽい性分ですし、このペースを維持するのは無理でしょうが、息切れした後はマイペースにやっていこうかと思ってます。

また、50バッジごとにSuperbadgeにチャレンジしていく、みたいな自分ルールを定めてます。予定だとこんな感じ。

バッジ数 superbadge
50 Apex Specialist
100 Reports & Dashboards Specialist
150 Security Specialist
200 Lightning Experience Specialist
250 Lightning Experience Rollout Specialist
300 Data Integration Specialist

もう一つの自分ルールとして、クリアすると決めたモジュールは必ずクリアして次のモジュールに移る、という風にしています。

Trailheadのよいところ

  • 業務で触る機会のない製品についても学べる
  • 難易度や所要時間など様々なモジュールが自由に選択できる
  • ゲーミフィケーションの要素を取り入れていて、バッジを集めていくのが楽しい
  • 分かったつもりの単元でも、意外と知らない気づきがある

改善点(?)とか

  • 説明は日本語訳されているのに、演習はすべからく英語なのが辛い
  • キャプチャ画像の文字が小さすぎて読めないことがあるので、クリックで拡大されると嬉しい(画像を別タブで開くと大きいキャプチャが見られるのに気づきませんでした…)
  • 演習で失敗した場合のメッセージが分かりにくい時がある

【一休 × JapanTaxi】サービスを支えるデータ分析基盤に行ってきた #ikyu_japantaxi

8/17(木)に開催された「【一休 × JapanTaxi】サービスを支えるデータ分析基盤」というイベントに行ってきました。

ikyu.connpass.com

資料は上記ページに公開されていますが、一応メモ。

一休.comを支えるデータ分析基盤

実践的な内容でよかったです。
AWS + GCP + Azureというマルチクラウドな分析基盤でした。

  • データ分析基盤環境を再構築した話
    • Before) データ分析を行う環境と本番環境が分断されていた(1日1回しかETLできないなど)。ディスク枯渇のおそれ。
    • After) ログ基盤 + データ分析基盤をクラウドで再構築
  • RedShift上に構築したところ、移行コストが高かった…(元々はSQLServerベース、RedShiftはPostgreSQLベース)
  • Azure SQL Data Warehouseを使うことに。
  • ログ基盤(ログ=ユーザのセッションログ・行動ログ)
    • JavaScript(Ajax) / fastly / API / Kinesis / Lambda / DynamoDB / 基幹DB
    • API部分はGoで実装(t2.small * 4で安定稼働中)
      • API Gateway & Lambdaも検討したが、性能面、安定面でGoがよかった
    • ログ集約部分はKinesis -> Lambda -> Cloud Storage
      • 相性は抜群
  • データ分析基盤
    • AWS(Kinesis / Lambda), GCP(Cloud Storage / BigQuery), Azure(Storage Blob / SQL Data Warehouse)というマルチクラウド構成
    • Lambda -> Azureを直でつなぐのは、Azureの制約でできなかった
  • 活用事例
    • KPI集計, CRM施策
    • 1 to 1マーケティング
      • Nヵ月ぶりにログインしたユーザにクーポンとか
      • プライスダウン通知
  • 今後
    • DynamoDBでリアルタイムに何かしたい(BigQueryだと8Hのタイムラグがある)

質疑応答

Q. 機微な会員情報のマスクは?
A. 会員データはMS SQL Server上にだけ存在する。ユーザにはvisitor IDが割り振られていて、紐付け情報だけDWHで持っている構成。

Q. コスト面やデータ伝送料について
A. BigQueryは安い。データ伝送料についてもそこまで高くはついていない。 ただ、RedShiftは1年のリザーブインスタンスで契約したものの、結局使わなくなったので、その分のコストは無駄になってしまった。。

Q. ログのトランザクション
A. 10,000弱/分。これでAPI(Go)のCPU負荷は7%くらい。

タクシー会社を支えるデータ収集技術 〜AWS Kinesis Stream/IoT の導入事例〜

Kinesisや動態管理など、自分の仕事と共通するところも多かったので、興味深く聞かせていただきました。

  • 従来:タクシー⇔配車サーバ⇒動態DB
    • 動態DB: 緯度、経度、ステータス(空車など)を管理するDB
    • 個社ごとに動態DBがあり、BigQuery、Embulkで動態DBをつないでいた(活用されていない動態DBがあったりする)
  • リアルタイムに、過去のデータも、簡単に、便利に、安全にアクセスできるデータ収集基盤が必要になった
  • Kinesis / Lambda / S3 / Athena で構成 ⇒ 失敗。。
    • Kinesisへの書き込みでレート超過が発生
  • ELB / Collector Server / Kinesis / Lambda / S3 / Athena
  • 話変わってAWS IoTの話
    • バイスの監視には細かいロギングが必要。消費電力を抑えるにはスリープを増やす必要がある ⇒ 対立する要求
    • AWS IoTを使うことでデバイスのロジックをクラウドに移すことができる

LT1 3分で作るストリーム処理基盤 ~Kafka + Flink on Docker編~

FlinkはオフィシャルのDockerイメージが、Kafkaも非公式だがDockerイメージがあるので、docker-compose upで簡単に基盤を作ることができる、という内容でした(デモ付)

LT2 クラシルを支える分析基盤 を支える話

社内で行っているSREエンジニア育成プログラム(アポロ)の話。
SRE本、最近購入したのですがまだ読めてないので、ちょっとずつ消化していかなきゃなぁと…。

  • 本番の障害対応は未経験者には難しく、経験者にだけ経験が集まり、未経験者(新しいSREエンジニア)が育たない悪循環。
  • ミッション1 本番環境と同党のログ収集基盤を構築(Fluentd, BigQuery, re:dash)
  • ミッション2 障害対応
    • ポイント1 過去に実際に起きたインシデントを元に再現
    • ポイント2 障害対応中のタイムラインを記録

LT3 SpinAppを支えるデータ収集基盤

EC2とRedShit中心とデータ収集基盤をGCPで作り直した話でした。結果的にコストは1/8になり、BigQueryも早くて安くてよかったとのこと。

ただ、エラーを返すのが多かったり、体感での障害が(AWSに比べて)多かったとのことでした。実際どうなんでしょうね…。

LT4 b→dashのデータ処理基盤

データ処理基盤のアーキテクチャ刷新のお話でした。
システム拡大に伴ってマイクロサービス化をしたり、データ容量の増大に伴ってスケーラビリティが容易な構成にしたり。
一方で、個社カスタマイズも必要になるとのことで、共通の部分と個別の部分を切り分けで設計されていたとのことでした。


というわけで、いろんな会社のデータ分析基盤について、実践的なアーキテクチャの話を聞くことができた勉強会でした。あと、会場の一休さんも自社から近いため、行きやすくてよかったです。
RedShiftはいいぞ、みたいな話がほぼなく、むしろBigQueryを使ってAWSGCP組み合わせたケースが多いんですね。

聞いた内容については、うまくプロジェクトにも還元していきたいです。

主催者、発表者、参加者の皆さま、おつかれさまでした。

Spring BootのRedisCacheManagerでJSONをいろんな型で利用する

Spring Bootのキャッシュ抽象化(Redis)でJSONを利用したい場合のメモです。

■ 目次

そもそもなんでJSON?

Redisの中身を直接調べる場合に、ヒューマンリーダブルであることが大きな理由です。デフォルトであるJavaのオブジェクトシリアライズ文字列だと、ほとんど中身が分からないので。

Configuration

先に結論ですが、Configurationは以下のようになりました。

やっていることは、

  • デフォルトとJSON用の2つの CacheManager を登録 *1
  • シリアライズでエラーがあった場合、無視して直接メソッドが呼ばれるように、カスタムのエラーハンドラを登録

の2つです。

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

    /**
     * デフォルトのCacheManager
     */
    @Bean
    @Primary
    CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        Map<String, Long> expires = new HashMap<>();
        // 略: キャッシュの有効期間を設定
    
        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        cacheManager.setExpires(expires);

        return cacheManager;
    }

    /**
     * JSON版のCacheManager
     */
    @Bean(name = "JsonCacheManager")
    CacheManager jsonCacheManager(RedisConnectionFactory connectionFactory) {
        ObjectMapper mapper = new ObjectMapper()
                .registerModule(new JavaTimeModule()) // LocalDateTime などの Date and Time API 関連のフィールドを扱う 
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) // 不明なプロパティがあっても無視
                .enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, "@class"); // デコード時の型の解決(後述)

        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // キーはシリアライズせずにただの文字列にする
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer(mapper)); // 値はJSONエンコード・デコードを利用する
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.afterPropertiesSet();

        Map<String, Long> expires = new HashMap<>();
        // 略: キャッシュの有効期間を設定

        RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
        cacheManager.setExpires(expires);
        return cacheManager;
    }

    @Override
    @Bean
    public CacheErrorHandler errorHandler() {

        return new SimpleCacheErrorHandler() {

            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                // キャッシュからのデシリアライズに失敗した場合は、エラーにはせずに対象のメソッドをそのまま呼ぶ(その結果はキャッシュされる)
                if (isSerializationError(exception)) {
                    return;
                }

                throw exception;
             }

            private boolean isSerializationError(RuntimeException exception) {
                if (exception instanceof SerializationException) {
                    return true;
                }

                Throwable cause = exception.getCause();
                return (cause != null && cause instanceof SerializationException);
            }
        }
    }
}

Jackson2JsonRedisSerializerとGenericJackson2JsonRedisSerializerの違いについて

CacheConfig の中では、 value serializer(Redisへの値の書き込み・読み込み手段)として GenericJackson2JsonRedisSerializer を使いました。

そもそもRedisTemplate に渡すJSON 用の value serializer には2種類あります。

  • Jackson2JsonRedisSerializer
  • GenericJackson2JsonRedisSerializer

両者の違いは、シリアライズ・デシリアライズの対象の型にあります。前者は特定の型に対してのものであり、後者はいろんな型( java.lang.Object とそのサブクラス)で使えます。
インスタンス作成方法の違いを見ると分かりやすいかもしれません。

Jackson2JsonRedisSerializer<Entity> x = new Jackson2JsonRedisSerializer<>(Entity.class);
x.setObjectMapper(mapper);

GenericJackson2JsonRedisSerializer y = new GenericJackson2JsonRedisSerializer(mapper);

Jackson2JsonRedisSerializer はコンストラクタ中でクラスを指定する必要があることから、結果的に、キャッシュが返す型ごとに CacheManager を用意する必要があり、煩雑になります。JSON用の CacheManager は1つにしたいので、 GenericJackson2JsonRedisSerializer を使っています。

ちなみに、

Jackson2JsonRedisSerializer<Object> x = new Jackson2JsonRedisSerializer<>(Object.class);

とすればいいじゃん?と思うかもしれませんが、これは、結局 GenericJackson2JsonRedisSerializer を使った場合と同じ挙動になります。

GenericJackson2JsonRedisSerializerを使うと、戻り値がLinkedHashMapになり、呼び出し時にキャストエラーになる

さてここで一つ問題があります。単純にGenericJackson2JsonRedisSerializerを使っただけだと、キャッシュから値が取得できた場合に戻り値が LinkedhashMap になってしまいます。
そうすると、メソッドを呼び出して結果を代入する箇所で ClassCastException(実行時エラー)が起きてしまいます。

具体的な例を示します。キャッシュを使ったメソッドの実装を以下のようにした場合 *2 に、

@Service
@RequiredArgsConstructor // Lombok
@Transactional(readOnly = true)
public class HogeService {

    private final HogeRepository hogeRepository;

    @Cacheable(value = "aaa", key = "'example:hoge:' + #key", cacheManager = "JsonCacheManager")
    public Hoge findByKey(String key) {
        return hogeRepository.findByKey(key);
    }
}

呼び出し側は

Hoge hoge = hogeService.findByKey("xxx");

となりますが、ここで、 LinkedHashMap から Hoge へのキャストが発生し、 ClassCastException が起きます。

キャッシュ内のJSON中に、型解決のためのヒントとなるプロパティを埋め込むようにする

上記の問題点の解決方法です。

まず、Spring BootというよりJacksonの話になりますが、デコード時の型として java.lang.Object を指定した場合、戻り値の型は、JSON文字列に応じて以下のようになります。

  • “1” とか “5.3” などの数値の場合 => IntegerDouble
  • “[ 3, 4 ]” などのリストの場合 => ArrayList
  • “{ "aaa” : “nnn” }“ のようなオブジェクトの場合 => LinkedHashMap

GenericJackson2JsonRedisSerializer の実装では、java.lang.Object を使ってデコードをしようとしていることから、キャッシュから値が取得できた時に LinkedHashMap になってしまい、戻り値の型 Hoge へのキャストが発生しているわけですね。

java.lang.Object ではなく、戻り値の型を使ってデコードしてくれればいいのに…」と思うわけですが、残念ながら RedisSerializer のデシリアライズのメソッドシグネチャは、

    T deserialize(byte[] bytes) throws SerializationException;

のようになっており、メソッドの引数から、戻り値の型を解決することができません。

じゃあどうすればよいか?と思うわけですが deserialize メソッドの引数はデコード対象のバイト列(JSON文字列)だけです。 なので、これを唯一の手がかりとして、JSON文字列の中に、具体的な型を解決するためのヒントを埋め込んであげればよさそうです。

キャッシュから値を読み込む時に、型情報を参照にする

CacheConfig 中の以下のコードが該当します。

objectMapper.enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, "@class"); 

これは「デコード時に指定された型が java.lang.Object の場合、JSON文字列中の “@class” プロパティをヒントにして型を解決してね」という意味です。なお、 “@class” の値には、クラスのFQCN文字列がセットされます。

ちなみに、JSON文字列中に “@class” プロパティが存在しない場合はエラーとなります。キャッシュの中身が不正なだけで、呼び出すたびにエラーになってしまうのは不便なので、カスタムエラーハンドラを作り、 SerializationException の時は無視してメソッドの呼び出しを行なうようにしています。

キャッシュに値が書き込む時に、型情報を付与する

クラスに @JsonType アノテーションを付けてあげます。

package sample;

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "@class")
public class Hoge {
    // 中身は省略
}

これにより、JSONエンコード時、すなわちキャッシュに値を書き込む時に、 @class というプロパティに “sample.Hoge” が付与されて書き込まれます。

まとめ

Spring BootのRedisCacheManagerでJSONをいろんな型で利用する場合のまとめです。

  • CacheManager を用意し、 value serializer に GenericJackson2JsonRedisSerializer を与える
  • キャッシュの戻り値として利用するクラスには @JsonTypeInfo を付けてあげる
  • GenericJackson2JsonRedisSerializer にセットする ObjectMapper には、型を解決するためのヒントを enableDefaultTypingAsPropertysetDefaultTyping メソッドで指定してあげる
  • キャッシュのデシリアライズに失敗した場合に備えて、 `CachingConfigurerSupport#errorHandler をオーバーライドしてカスタムのエラーハンドラを作り、デシリアライズエラー時はキャッシュを諦めてメソッドを呼ぶようにした方が、(おそらく)安全

*1:複数の CacheManager を統合する CompositeCacheManager というのもありますが、今回は簡略化のため複数利用しました。

*2:キャッシュの使い方としてちょっと古いバージョンの実装かもしれません。。