re:Inventの流れが早くて見逃していたのですが、AWS CodeBuildで依存関係のキャッシュ機構が使えるようになっていました。
昔、自前で似たようなことをするためのやり方を試行錯誤してたのですが、もう不要ですね。
あとあるとすれば、ビルドにカスタムDockerイメージを使っている場合に、イメージのダウンロードに時間がかかってしまう課題が解消できると、最強な気がします。
re:Inventの流れが早くて見逃していたのですが、AWS CodeBuildで依存関係のキャッシュ機構が使えるようになっていました。
昔、自前で似たようなことをするためのやり方を試行錯誤してたのですが、もう不要ですね。
あとあるとすれば、ビルドにカスタムDockerイメージを使っている場合に、イメージのダウンロードに時間がかかってしまう課題が解消できると、最強な気がします。
Advent Calendarの季節になりましたね。世のエンジニアの1年間のノウハウが放出されるイベント、楽しみです。
さてそんな中、Advent Calendarとまったく関係のない記事になりますw(エントリを逃したとも言う…)
SFDC謹製のJavaのAPIクライアントであるWSC (Force.com Web Service Connector)を使って、ちょっとクセのある(?)「地理地情報型(Location)」や、Summer'17でパイロットリリースされた「時間データ型(Time)」を操作する時のやり方についてのメモです。
先に結論ですが、それぞれ以下のように扱うことで、利用できます。
FooLocation__c
の場合は、 緯度が FooLocation__Latitude__s
, 経度が FooLocation__Longitude__s
のようになる地理位置情報型、以下のIssueにある通り、少し前まではダウンロードしてきたWSDLに手を加えないと使えなかったのですが、今は直っているみたいですね*1。
また、時間データ型についてはベータなので、今後変更になる可能性もある気もしますが、現時点での結果ということで…。
レコード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()); } } }
Spockでテストコードを書く時の備忘録。
メソッド
public void method1(List<String> list, Consumer<List<String>> consumer)
hoge.method1(foo, list -> list.add("aaa"));
Groovy
hoge.method1(foo, { list -> list.add('aaa') })
メソッド
public void method2(String text, Predicate<String> predicate)
hoge.method2("test", s -> s.length() < 5);
Groovy
hoge.method2('test', { s -> s.length() < 5 }) // size() でもOK
メソッド
public void method3(Supplier<String> supplier)
hoge.method3(() -> "test");
Groovy
hoge.method3({ 'aaa' }) // -> 'aaa' hoge.method3({ -> 'bbb' }) // -> 'bbb'
メソッド
public void method4(String text, Function<String, String> function)
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式がそのまま使えるみたいです(このブログを書いた直後に偶然知りました)
10月も終わろうとしていますが、依然とほぼ同じペースで時間を確保することができ、大分進みました。
50個ごとにSuperbadgeにチャレンジするという計画は、100個取った時点で軌道修正しました。
そもそも今ある全部のバッヂを足しても300行かなそうなので…。というわけで、週末を利用してSuperbadgeはコンプリートしました。
必然的に日本語訳されていないモジュールに取り組むことも多くなりましたが、英語と日本語で内容が違っているのとかは相変わらずでしたね。
特に、Lightningコンポーネント関連のモジュールは多かった気がします。それだけ発展途上ってことなのかなと思いました。
今後は、ちょっとペースを落としつつ、お気に入りしているボリュームのあるモジュール(とプロジェクト)を消化していこうかなぁと思います。
ずっと食わず嫌いで手を付けていなかった、というわけでもないのですが、ドラクエ11も裏ボスまで倒してやることがほぼなくなったということもあり、Trailheadをはじめました。
やってみると、結構楽しいw
今のところのスコアはこんな感じ。
トレイルが少ないのは、手を付けやすそうなモジュールからつまみ食いをしていたためだと思われます。
もうちょっと体系立てて選んでいけばよかったなと反省。。
勉強時間は、平日は22時~1時くらい。休日は日によってまちまちですが、多くても6時間くらい。(大体ドラクエ11をやっていた時間ですね)
飽きっぽい性分ですし、このペースを維持するのは無理でしょうが、息切れした後はマイペースにやっていこうかと思ってます。
また、50バッジごとにSuperbadgeにチャレンジしていく、みたいな自分ルールを定めてます。予定だとこんな感じ。
バッジ数 | superbadge |
---|---|
50 | |
100 | Reports & Dashboards Specialist |
150 | Security Specialist |
200 | Lightning Experience Specialist |
250 | Lightning Experience Rollout Specialist |
300 | Data Integration Specialist |
もう一つの自分ルールとして、クリアすると決めたモジュールは必ずクリアして次のモジュールに移る、という風にしています。
8/17(木)に開催された「【一休 × JapanTaxi】サービスを支えるデータ分析基盤」というイベントに行ってきました。
資料は上記ページに公開されていますが、一応メモ。
実践的な内容でよかったです。
AWS + GCP + Azureというマルチクラウドな分析基盤でした。
Q. 機微な会員情報のマスクは?
A. 会員データはMS SQL Server上にだけ存在する。ユーザにはvisitor IDが割り振られていて、紐付け情報だけDWHで持っている構成。
Q. コスト面やデータ伝送料について
A. BigQueryは安い。データ伝送料についてもそこまで高くはついていない。
ただ、RedShiftは1年のリザーブインスタンスで契約したものの、結局使わなくなったので、その分のコストは無駄になってしまった。。
Q. ログのトランザクション量
A. 10,000弱/分。これでAPI(Go)のCPU負荷は7%くらい。
Kinesisや動態管理など、自分の仕事と共通するところも多かったので、興味深く聞かせていただきました。
FlinkはオフィシャルのDockerイメージが、Kafkaも非公式だがDockerイメージがあるので、docker-compose upで簡単に基盤を作ることができる、という内容でした(デモ付)
社内で行っているSREエンジニア育成プログラム(アポロ)の話。
SRE本、最近購入したのですがまだ読めてないので、ちょっとずつ消化していかなきゃなぁと…。
EC2とRedShit中心とデータ収集基盤をGCPで作り直した話でした。結果的にコストは1/8になり、BigQueryも早くて安くてよかったとのこと。
ただ、エラーを返すのが多かったり、体感での障害が(AWSに比べて)多かったとのことでした。実際どうなんでしょうね…。
データ処理基盤のアーキテクチャ刷新のお話でした。
システム拡大に伴ってマイクロサービス化をしたり、データ容量の増大に伴ってスケーラビリティが容易な構成にしたり。
一方で、個社カスタマイズも必要になるとのことで、共通の部分と個別の部分を切り分けで設計されていたとのことでした。
というわけで、いろんな会社のデータ分析基盤について、実践的なアーキテクチャの話を聞くことができた勉強会でした。あと、会場の一休さんも自社から近いため、行きやすくてよかったです。
RedShiftはいいぞ、みたいな話がほぼなく、むしろBigQueryを使ってAWSとGCP組み合わせたケースが多いんですね。
聞いた内容については、うまくプロジェクトにも還元していきたいです。
主催者、発表者、参加者の皆さま、おつかれさまでした。
Spring Bootのキャッシュ抽象化(Redis)でJSONを利用したい場合のメモです。
■ 目次
Redisの中身を直接調べる場合に、ヒューマンリーダブルであることが大きな理由です。デフォルトであるJavaのオブジェクトシリアライズ文字列だと、ほとんど中身が分からないので。
先に結論ですが、Configurationは以下のようになりました。
やっていることは、
の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); } } } }
CacheConfig
の中では、 value serializer(Redisへの値の書き込み・読み込み手段)として GenericJackson2JsonRedisSerializer
を使いました。
そもそもRedisTemplate
に渡すJSON 用の value serializer には2種類あります。
両者の違いは、シリアライズ・デシリアライズの対象の型にあります。前者は特定の型に対してのものであり、後者はいろんな型( 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
になってしまいます。
そうすると、メソッドを呼び出して結果を代入する箇所で 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
が起きます。
上記の問題点の解決方法です。
まず、Spring BootというよりJacksonの話になりますが、デコード時の型として java.lang.Object
を指定した場合、戻り値の型は、JSON文字列に応じて以下のようになります。
Integer
や Double
ArrayList
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
には、型を解決するためのヒントを enableDefaultTypingAsProperty
や setDefaultTyping
メソッドで指定してあげる`CachingConfigurerSupport#errorHandler
をオーバーライドしてカスタムのエラーハンドラを作り、デシリアライズエラー時はキャッシュを諦めてメソッドを呼ぶようにした方が、(おそらく)安全