Hive0.8のfrom_utc_timestamp, to_utc_timestampの不具合を修正する

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

 

新しいサイトもよろしくお願いします!