HadoopアドベントカレンダーでTimestampの機能について書いた時に見つけた不具合を調査してみました。 自分の備忘録用にメモメモ。
from_utc_timestampまたはto_utc_timestampをSELECT句に複数書くと値がおかしくなるというものです。正確には、1つ書いても壊れます。まずは再現コード。
create table ts1(t1 timestamp); load data local inpath '/mnt/iscsi/test1/data4.txt' overwrite into table ts1;
select t1 from ts1; 2011-12-25 09:00:00
select from_utc_timestamp(t1, 'JST') from ts1 limit 1; 2011-12-25 18:00:00
うまくいっているように見えますが、、
select t1, from_utc_timestamp(t1, 'JST'), t1 from ts1 limit 1; 2011-12-25 18:00:00 2011-12-25 18:00:00 2011-12-25 18:00:00
おや、全部の値が変わってしまいました。さらに、from_utc_timestamp(t1, 'JST')を追加してみます。
select t1, from_utc_timestamp(t1, 'JST'), t1, from_utc_timestamp(t1, 'JST') from ts1 limit 1; 2011-12-26 03:00:00 2011-12-26 03:00:00 2011-12-26 03:00:00 2011-12-26 03:00:00
SELECT句で書いたt1が全て同じ値になります。
次に実装クラスを見てみます。org.apache.hadoop.hive.ql.udf.genericパッケージのGenericUDFFromUtcTimestampとGenericUDFToUtcTimestampです。GenericUDFToUtcTimestampはGenericUDFFromUtcTimestampを継承していて、フラグに応じてOffset値の正負を反転させているだけです。で、問題はGenericUDFFromUtcTimestampのapplyOffsetメソッドです。ソースを下に書きました。
@Override public Object evaluate(DeferredObject[] arguments) throws HiveException { Object o0 = arguments[0].get(); TimeZone timezone = null; if (o0 == null) { return null; } if (arguments.length > 1 && arguments[1] != null) { Text text = textConverter.convert(arguments[1].get()); if (text != null) { timezone = TimeZone.getTimeZone(text.toString()); } } else { return null; } Timestamp timestamp = ((TimestampWritable) timestampConverter.convert(o0)) .getTimestamp(); int offset = timezone.getOffset(timestamp.getTime()); if (invert()) { offset = -offset; } return applyOffset(offset, timestamp); } protected Timestamp applyOffset(long offset, Timestamp t) { long newTime = t.getTime() + offset; int nanos = (int) (t.getNanos() + (newTime % 1000) * 1000000); t.setTime(newTime); t.setNanos(nanos); return t; }
行を評価中、timestamp変数は同じインスタンスを毎回返してきます。そのインスタンスに対してapplyOffsetでsetTimeしちゃってます。つまり、その行を評価中はt1は同じインスタンスを使い回してしまいます。なのでSELECT句に書いたOffset値全てが計算された結果が出力されるわけです。そして、1列評価→出力→1列評価→出力ではなく、全ての列を評価→出力なので、出力時には全て同じtimestampインスタンスを指すことになって、全て同じ値が表示されるわけです。(ここで言う全てはt1を参照している列という意味です)
修正方法ですが、結局のところは同じインスタンスを指せないという事で、インスタンスを生成する必要が出てきます。というわけで、手っ取り早く修正するのであればapplyOffsetを修正します。以下が修正版です。
protected Timestamp applyOffset(long offset, Timestamp t) { long newTime = t.getTime() + offset; int nanos = (int) (t.getNanos() + (newTime % 1000) * 1000000); Timestamp t2 = new Timestamp(newTime); t2.setNanos(nanos); return t2; }
(追記)上記のコードはダメでした。nano時間のところがそもそもバグっていて秒未満の値がずれます。以下のコードの方が良いです。(timestamp_udf.q.outの秒未満部分もずれてました・・)
protected Timestamp applyOffset(long offset, Timestamp t) { long newTime = t.getTime() + offset; Timestamp t2 = new Timestamp(newTime); t2.setNanos(t.getNanos()); return t2; }
hiveをビルドしなおして再度実行してみます。
select t1, from_utc_timestamp(t1, 'JST'), t1, from_utc_timestamp(t1, 'JST') from ts1 limit 1; 2011-12-25 09:00:00 2011-12-25 18:00:00 2011-12-25 09:00:00 2011-12-25 18:00:00
直りました。
さて、Hadoop、Hiveのコミュニティっていきなりチケットを開いてもいいのかな。ちょっとMLとJIRAを漁って作法を調べよう。
チケット開く前にパッチを置いておこう。Javaソースとテスト用のqファイル、q.outファイルのパッチは分離して2つに分けた方が良かったかな・・。
http://mt.orz.at/archives/HIVE-xxxx.1.patch.txt