Redmine APIによるデータ取得とjqで捗る話

8月 13th, 2014

RedmineのREST APIとjqコマンドで今更ながら色々捗るなーという話。

Redmine API

Redmine exposes some of its data through a REST API. This API provides access and basic CRUD operations (create, update, delete) for the resources described below. The API supports both XML and JSON formats.

jq

jq is like sed for JSON data – you can use it to slice and filter and map and transform structured data with the same ease that sed, awk, grep and friends let you play with text.

環境

jq version 1.3
Redmine version 2.5.2.stable

こんなチケットを作ってみて動かします。

チケット一覧画面

データを取ってくる

RedmineAPIはリクエストにAPI KEY(ユーザー管理画面より発行)を含めることで認証出来ます。

passed in as a “key” parameter
passed in as a username with a random password via HTTP Basic authentication
passed in as a “X-Redmine-API-Key” HTTP header (added in Redmine 1.1.0)

とドキュメントにあるように、三通りの方法があります。こんなに対応せんでもええんやないか、と思います。curlで叩いてみます。

keyパラメータを使用

curl -s http://redmine.mydomain.com/issues.json?key={myapikey}

Basic認証でユーザー名として使用(randome passwordって書いてああるけど無視されるので空でもよい)

curl -s http://{myapikey}@redmine.mydomain.com/issues.json

or

curl -s -u {myapikey}: http://redmine.mydomain.com/issues.json

X-Redmine-API-Key認証ヘッダを使用

curl -H "X-Redmine-API-Key:{myapikey}" -s http://redmine.mydomain.com/issues.json

認証に成功するとissuesのJSONがごちゃっと返ってきます。
そのままでは見づらいのでとりあえずjqに食わせてみます。

jqに渡してissuesの1件目を表示

curl -s  http://{myapikey}@redmine.mydomain.com/issues.json? | jq '.issues[0]'
{
  "updated_on": "2014-08-08T10:22:32Z",
  "created_on": "2014-08-08T10:19:40Z",
  "estimated_hours": 5,
  "done_ratio": 0,
  "start_date": "2014-08-08",
  "description": "機能追加タスク",
  "id": 4,
  "project": {
    "name": "myproject",
    "id": 1
  },
  "tracker": {
    "name": "機能",
    "id": 2
  },
  "status": {
    "name": "新規",
    "id": 1
  },
  "priority": {
    "name": "通常",
    "id": 2
  },
  "author": {
    "name": "Redmine Admin",
    "id": 1
  },
  "assigned_to": {
    "name": "Redmine Admin",
    "id": 1
  },
  "subject": "機能追加"
}

見やすいですね。

絞り込みとか加工とか

データが取れたので色々加工してみます。

自分のタスク情報を取得

自分にアサインされたissuesを取得してjqで表示項目を絞り込んでみる。

curl -s  http://{myapikey}@redmine.mydomain.com/issues.json?assigned_to_id=me | jq '.issues[] | {subject, id, assigned_to, estimated_hours}'
{
  "estimated_hours": 3,
  "assigned_to": {
    "name": "egmc .com",
    "id": 3
  },
  "id": 2,
  "subject": "機能追加だよ"
}
{
  "estimated_hours": 1.5,
  "assigned_to": {
    "name": "egmc .com",
    "id": 3
  },
  "id": 1,
  "subject": "バグだよ1"
}

文字列を整形してリストっぽくデータを取ってみる

jqは文字列内に取得したデータを入れ込んだり、各種演算子も使えたりするのでこんな出力も得られます。

curl -s http://{myapikey}@redmine.mydomain.com/issues.json?assigned_to_id=me | jq -r '.issues[] | "・#\(.id) \(.subject)[担当:\(.assigned_to.name)](\(.estimated_hours)h)"'
・#2 機能追加だよ[担当:egmc .com](3h)
・#1 バグだよ1[担当:egmc .com](1.5h)

作成済のカスタムクエリの条件を適用してみる

redmine側で作成した既存のカスタムクエリの出力をjsonで得ることも出来ます。
issuesのAPIにはないのですが、下記のように画面表示と同じURL構造にして/projects/{プロジェクト名}/issues.json?query_id={クエリID}というリクエストを送るとカスタムクエリの条件が適用されます。
というか単純に画面でリストを表示して、拡張子を.jsonとか.xmlにすることによってその形式で出力してくれるようです(なのでこちらは厳密にはAPIで提供されてるわけではないっぽい)

「担当が自分で見積もり時間が2以上かかるタスク」という条件のカスタムクエリで試してみます。

カスタムクエリ

curl -s http://{myapikey}@redmine.mydomain.com/projects/myproject/issues.json?query_id=3 | jq '.issues[] | {subject, id, assigned_to, estimated_hours}'
{
  "estimated_hours": 3,
  "assigned_to": {
    "name": "egmc .com",
    "id": 3
  },
  "id": 2,
  "subject": "機能追加だよ"
}

コマンドを組み合わせて集計とか

パイプでawkなどに渡すことによって簡単な集計処理も出来ます。

見積もり時間の合計を取得

curl -s http://{myapikey}@redmine.mydomain.com/issues.json?assigned_to_id=me | jq '.issues[].estimated_hours' | awk '{m+=$1} END{print m}'
4.5

特定の期間内のチケットの集計とか工夫次第で色々出来そうですね。

まとめ

RedmineAPIとjqの組み合わせでコマンドラインから色々出来るのでベンリだよ!という事を言いたかった。
とはいえ各種コマンドとの組み合わせでもうちょっとベンリに使える気がするのでシェル力アップのためにもうちょっと勉強しようと思います。


CakePHP1.3.20、2.5.3で修正されたデータが全件更新されるやつを調べてみた

8月 8th, 2014

http://bakery.cakephp.org/articles/markstory/2014/07/21/cakephp_2_5_3_and_1_3_20_released

7/21に久々に1.3系を含むCakeのアップデートがリリースされました。
1.3系も更新されるやつは大体やばいやつ、ということで今回も何やらやばそうな事が書いてあります。

The 1.3.20 release contains an important fix to address a potential race condition in Model::save() that can cause data loss when records are deleted during concurrent updates.

data lossとはどういうことか、実際に試して確認してみました。

結論

save(update)処理の最中に対象レコードが削除されると、〜where1=1というえらいクエリが発行されて更新しようとした内容で全件updateが発生します。
速やかにバージョンアップしましょう。1.3系は他に変更点はなさそうです。

環境

PHP5.4.31
CakePHP1.3.19(再現環境)

試してみる

system_logsというテーブルに対してID指定で更新をかける下記のようなコードを実行。

$this->SystemLog->id = 100;
$updatedata = [];
$updatedata['SystemLog']['user_id'] = 1;
$this->SystemLog->save($updatedata);

id=100のレコードが存在する場合

下記のようなクエリが実行される。


SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

UPDATE `system_logs` SET `user_id` = 1  WHERE `system_logs`.`id` = 100

カウントが二回実行されてますがどちらも存在チェック(model::exits())で呼ばれてます。
普通にアップデートされてますね。

id=100のレコードが存在しない場合

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

INSERT INTO `system_logs` (`user_id`, `created`) VALUES (1, '2014-08-07 19:00:29')

最初の存在チェックでレコードが存在しないため、insertが実行されました。

id=100のレコードが更新途中で削除された場合

初回の存在チェックと、二回目の存在チェック(DboSource::defaultConditions)より前にsleepを入れてレコード削除。

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

SELECT COUNT(*) AS `count` FROM `system_logs` AS `SystemLog`   WHERE `SystemLog`.`id` = 100

UPDATE `system_logs` SET `user_id` = 1  WHERE 1 = 1

WHERE1=1というやばそうなUPDATEが実行されました。

何が起きているのか?

ID指定ありで、Modell::save内の初回の存在チェックが通るとupdateとして処理が行われるのですが、その際のcondition(WHERE句)作成ロジック内で再度対象レコードの存在チェックが行われ、対象レコードが存在しないとdefaultCondition=nullとして扱われ、WHERE 1 = 1というSQLが発行されるようです。

dbo_source.phpの1877行あたり

   function defaultConditions(&$model, $conditions, $useAlias = true) {
        if (!empty($conditions)) {
            return $conditions;
        }
        $exists = $model->exists();
        if (!$exists && $conditions !== null) {
            return false;
        } elseif (!$exists) {
            return null; // 存在チェックに引っかかるとnullが返り、結果的にwhere1=1となる
        }
        $alias = $model->alias;

        if (!$useAlias) {
            $alias = $this->fullTableName($model, false);
        }
        return array("{$alias}.{$model->primaryKey}" => $model->getID()); //通常はこっち
    }

アップデート版の挙動

以下のように修正されてます。
$model->__safeUpdateModeはsaveによる更新時はtrueとなります。

    function defaultConditions(&$model, $conditions, $useAlias = true) {
        if (!empty($conditions)) {
            return $conditions;
        }
        $exists = $model->exists();
        if (!$exists && ($conditions !== null || !empty($model->__safeUpdateMode))) {
            return false;
        } elseif (!$exists) {
            return null;
        }
        $alias = $model->alias;

        if (!$useAlias) {
            $alias = $this->fullTableName($model, false);
        }
        return array("{$alias}.{$model->primaryKey}" => $model->getID());
    }

存在チェックに引っかかった場合はnullではなくfalseが返るようになります。
defaultConditionsでfalseが返された場合、実行されるSQLは以下のようになります。

UPDATE `system_logs` SET `user_id` = 1  WHERE 0 = 1

お、おう、という感じはしますが結果的に更新はかからなくなります。

影響は?

同一レコードに対する更新、削除が頻繁に行われるようなシステムでなければ(例えば特定のユーザーに紐付いたデータ等であれば)そんなに起こることはなさそうですが、バッチによる一斉更新とユーザー操作がかぶったりした場合に運悪いと遭遇しそうです。
速やかにアップデートしましょう。

最後に

nullとかfalseとかarrayとかが混在して戻ってくるやつをアレコレするの見てて辛い。


株式会社Speeeにてサービス開発と運用の話をしてきました

2月 22nd, 2014

ちょっとした御縁があり、株式会社Speee様の社内勉強会にてお話させて頂きました。

エンジニア1名によるサービス開発と運用

エンジニア以外も参加するので技術的な話だけじゃなくて運用の話でもOK、というオーダーを頂いた結果、コードの全然出てこない内容となりました。
「こんな内容で大丈夫か」と再々事前に聞いたんですが「大丈夫だ、問題ない」と言われたのでそのまんまやってます。

合間に質問どうぞ的なスライドを何枚か入れていて、内容的にスルーされたらやべーな、と思ってましたが割とツッコミを頂いて、喋ることとなりました。むしろ回答がぐだぐだで申し訳なかったなーと反省。

Speeeのエンジニアの方々は各々個性があって技術好きっぽい空気が出ていたのでぼっち開発してる身としてはまあ羨ましいですね。

勉強会後にエンジニアの方々と話していたら、当初は技術的な話をしていたものの最終的にアイドルとアニメの話になっていた点についても反省しつつ、なかなかおいしいコーヒーを出してもらったり諸々いい感じに対応してもらってありがたいことでした。

サービス運用の話はうまいことまとめるのが難しいですが、これはこれで一つの事例として、機会があれば違う切り口でやってみたいところ。

【追記】
techブログにて紹介頂きました。
株式会社ハイパーインターネッツの岩堀さんに「CAMPFIREの開発と運用」についてお話いただきました。