技術的な話題 に関する投稿を表示しています

PHP で時間を固定した未来にしたりしたい

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

PHP で時間が絡むようなテストをしていてコケたりコケなかったりした。

具体的には、登録してから〇〇時間後にあるメソッドが呼ばれたら、ステータスを xx に変える、みたいなもの。

そんなときに使えるアイデアを3つ。

Carbon を使っている場合

Carbon なら setTestNow を使うと良い。
各テストで、好きなように setTestNow を書く。

Carbon::setTestNow(Carbon::parse('ここに時刻'));

特に時刻の希望がなく、現在時刻を固定したいだけならこれで。

Caron::setTestNow(Carbon::now());

それと tearDown で引数なしで呼べばよい(事故防止)

public function tearDown()
{
    parent::tearDown();
    Carbon::setTestNow();
}

Carbon - A simple PHP API extension for DateTime.

Chronos を使っている場合

Chronos にも Carbon と同様に setTestNow が用意されている。助かる〜。

使い方は Carbon のソレと一緒。

Chronos - 3.6

php-timecop を導入する

特に何も導入してなくて DateTime をもりもりやってたり、もはや date や time が乱立してたらこれしかお手軽な手は無いか。
PHP 拡張として動作して PHP が扱う時間をコントロールするツワモノ。

テスト用途だけじゃなくて、例えばあるページが未来ではどのように見えるかの動作確認用とか、プロダクション用途としても使える。(使ったことがある)

remi や brew pecl で配布されているので導入もお手軽。

GitHub - hnw/php-timecop: A PHP extension providing "time travel" capabilities inspired by ruby timecop gem

(余談だけど mac 上でやろうとしたら homebrew の php が core にくっついて拡張の類がなくなったので pecl から入れる必要があった)

ざっくりとはこんな感じ strtotime でもいい。

var_dump(date('Y-m-d H:i:s'));
sleep(3);
var_dump(date('Y-m-d H:i:s'));

timecop_freeze(new DateTime('2018-01-01 12:00:00'));

var_dump(date('Y-m-d H:i:s'));
sleep(3);
var_dump(date('Y-m-d H:i:s'));

timecop_return();

var_dump(date('Y-m-d H:i:s'));
sleep(3);
var_dump(date('Y-m-d H:i:s'));
string(19) "2018-06-14 12:05:05"
string(19) "2018-06-14 12:05:08"
string(19) "2018-01-01 12:00:00"
string(19) "2018-01-01 12:00:00"
string(19) "2018-06-14 12:05:11"
string(19) "2018-06-14 12:05:14"

そのほか

そもそもの設計として、外からオブジェクトをインジェクション出来るようにするほうが何かと便利っぽい。
とはいえ無理な状況ってあると思うので、そういうときにここで挙げたアイデア使えるんじゃないかな。timecopとかすごい。

authorization と authentication の話。そして Laravel の Gate の紹介をちらっと。

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

要するに、認証と認可は別物だよって話。

 

ざっくりとまとめると。

認証 Authentication
あなたは誰ですか
私は X というユーザです、パスワードはこれです
確認しました X で間違いないです

 

認可 Authorization
X です、管理画面を開きたいです
あなたは管理画面への"鍵"をもっていません、拒否します

 

もうちょい例。
世帯主じゃなくても、家の鍵を持っていれば家に入れる。=認可
世帯主かどうかは公的文書(住民票とか)で確認する。=認証

 

で、じゃあ Web ではどうやんねん、というと、
(例によってよく使うものだけ紹介)

CakePHP ならこのあたりにまとまってる。

認証 - 3.6

 

Laravel だと分かれてる。使う仕組みが違うので別項目になってるって。

Authentication - Laravel - The PHP Framework For Web Artisans
Authorization - Laravel - The PHP Framework For Web Artisans


Laravel の Gate の説明をちゃんと見てみたの初めてだけど Gate かっちょいいな、権限管理とか、できるできないみたいな話がわりかし簡単にかけそう。

できるできないはこういう風に AuthServiceProvider->boot に書いたり。

Gate::define('update-post' function ($user $post) {
    return $user->id == $post->user_id;
});

こういう形で クラス名 @ メソッド名 として外だししたり。

Gate::define('update-post' 'PostPolicy@update');
class PostPolicy
{
    public function update(User $user Post $post)
    {
        return $user->id == $post->user_id;
    }
}

でもって、定義した Gate は、コントローラーだったりモデルだったりで、こうやって使えるらしい。

if (Gate::forUser($user)->allows('update-post' $post)) {
    // The user can update the post...
}

あとは、ポリシーを定義することができて(↑にある PostPolicy@update という書き方で使えるものとは異なる)
Eloquant に対して view/create/update/delete がそれぞれ権限ありますか?というのをお手軽に内部でつないでくれるらしい。

たぶん実際のユースケース的にはいろいろ分割しやすいこの書き方がいいんじゃなかろうか。

AuthServiceProvider はこういう感じで

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];
...

ポリシークラスのほうはこんな感じ。

namespace App\Policies;

use App\User;
use App\Post;

class PostPolicy
{
    public function update(User $user Post $post)
    {
        return $user->id === $post->user_id;
    }
}

さっきの update とまったく同じなんだけど、こうすることで AuthServiceProvider が勝手に Post を監視してくれて PostPolicy に書かれている通り update 時のチェックを実行して true なら update ができる、というようなことをやってくれるんだそうだ。

便利~~~~。

 

例えば wordpress 的なものをイメージして、簡単にやってみると。

  • 投稿
    • 管理者でログインしていれば投稿の作成、編集、閲覧、公開設定にすることが可能
    • 編集者でログインしていれば投稿の作成、編集、閲覧、が可能。公開設定にはできない
    • ログインしていなければ投稿の閲覧が可能
  • コメント
    • 管理者でログインしていればコメントの作成、削除、公開されたコメントの閲覧、管理者向けコメントの閲覧が可能
    • それ以外は作成と、公開されたコメントの閲覧のみ可能

みたいなのも Gate をつかったらぱっぱっぱ~~~ってかけそうだって思った。
投稿ポリシーとコメントポリシーを、それぞれ書いてある通りに定義すればいいだけだし、仕様を形にするのが簡単そうだ。

んん~~~、使いどころがあれば使いたいなあ。

PhpStorm でコーディングしてるときに連番を作りたいなーと思ったけど、それ seq でええやんって解決した

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

SQLを発行したり、テストデータを作ったり、絶妙なタイミングで連番の出番がある気がする。
ので、そうなったときに、一旦適当な php か html ファイルを開いて Emmet の機能を使ってしまうのが簡単だなあ、と思った。

a[class=$$$$$$]*100

まで入力して Tab を押すとどばーって展開される。上限は 100 っぽい。

ここからあとは 検索 → マルチカーソルでの選択 → タグの部分を削除 ということをすると連番だけいい感じに作れる。よい。

PhpStorm …というか多分 IntelliJ 系ならできるんじゃなかろうか? PhpStorm しか使ってなくて他わからん。

 

…。

と思ったんですけど、冷静に考えて seq と pbcopy でいいんじゃなかろうか。

seq -f '%06g' 1 100 | pbcopy

以下の内容がクリップボードに入ってるので、そのまま貼り付けられる。
000001
000002
......
000100

おわり。


余談だけど作業環境が Mac に切り替わったので Windows の話はわからん。
gocopy なるものがあるらしいのでそれとかどうだろう。デフォルトだと clip ってコマンドが使えるらしい。

atotto/clipboard: clipboard for golang

MySQL で ERROR 1265 (01000): Data truncated for column ‘xxxxx’ at row 1 みたいなエラーが出たとき

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

ぼくは UPDATE 文を実行しようとしただけなんだ。

> update xxxxx set yyyyy = "aaaaa";
ERROR 1265 (01000): Data truncated for column 'yyyyy' at row 1

enum 型のカラムに列挙されてない値を入れようとしたら出たものです。

 

というわけで ALTER して取りうる値の種類を増やすと、クエリが通りました。

ALTER TABLE テーブル名 MODIFY COLUMN カラム名 ENUM(取りうる値);

CHANGE COLUMN でもいいですが、書き方が微妙に違います。
古いカラム名、新しいカラム名、という書き方になります。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 13.1.7 ALTER TABLE 構文

 

ちなみにこのエラー、他にもカラムの幅が足りないとこに入れようとしても出るようです。

mysql - Data truncated for column? - Stack Overflow

git の差分で何かをした(今回は phpunit をしたい)

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

テストをもりもり追加したけど、それだけいい感じに動かしたいなあってやつ。

キモはこのコマンド。

git diff --name-only 比較元ブランチ 比較対象ブランチ

--name-only をつけると変更のあったファイル名だけが出力される。

ちなみにブランチと書いたものの、コミットが示せればいい。
HEAD とか HEAD~2 とか。
あるいは abc123 みたいにハッシュとか。

そしたらこいつをパイプしながらゴニョゴニョすればおけ。

 

ここ最近、手元で使ってみてるのはこれ。

$ git diff --name-only HEAD $(git branch -a | peco | tr -d '*' | tr -d ' ') | grep -i "test.php" | xargs -I '{}' composer test '{}' | grep ')'
> phpunit --testsuite unit 'tests/Hoge/HogeTest.php'
.                                                                   1 / 1 (100%)
OK (1 test, 7 assertions)
> phpunit --testsuite unit 'tests/Hoge/HugaTest.php'
.......S.S...S.                                                   15 / 15 (100%)
> phpunit --testsuite unit 'tests/Hoge/FooTest.php'
.                                                                   1 / 1 (100%)
OK (1 test, 1 assertion)
> phpunit --testsuite unit 'tests/Hoge/BarTest.php'
........                                                            8 / 8 (100%)
OK (8 tests, 15 assertions)

peco が必要。
peco/peco: Simplistic interactive filtering tool

実行すると、ブランチを指定できて(リモート可)、ワーキングディレクトリとの差分があったテストだけ実行できる。 master ばかりではなくて、たまには違うブランチもやりたいよね。


と、思ったけど、ソースだけ変えた場合って出てこないよね…。
うまいこと差分に対してテストできるような仕組みできないかなー

ast 作れるし、いい感じに解析して、影響のありそうなテストだけいい感じに動かしてくれる君みたいなの。

WordPress 環境で nginx の proxy_cache を有効にする

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

おぼえがき。

Module ngx_http_proxy_module

キャッシュの名前、その保存先、サイズを設定する。

// /etc/nginx/nginx.conf
http {
    ...

    # cache
    proxy_cache_path /tmp/nginx/cache keys_zone=cache1:10m;
}

サイズについては公式ドキュメントにこう書いてあります。

One megabyte zone can store about 8 thousand keys.

10m という指定だとざっくり 8 万個のキーが保存できそうです。

どういうときにキャッシュするかを設定する

いくつかポイントがあるが、とりあえず今回の設定内容を出します。

// /etc/nginx/conf.d/server.conf
server {
    ...

    # cache
    set $mobile "";
    if ($http_user_agent ~* '(Mobile|Android)') {
        set $mobile "SP";
    }

    set $do_not_cache "";
    if ($http_cookie ~* "comment_author_|wordpress_(?!test_cookie)|wp-postpass_" ) {
        set $do_not_cache 1;
    }

    proxy_no_cache          $do_not_cache;
    proxy_cache_bypass      $do_not_cache;
    proxy_cache             cache1;
    proxy_cache_key         "$mobile//$scheme://$host$request_uri$is_args$args";
    proxy_cache_valid       200 301 302 30d;
    proxy_cache_valid       any 10m;

    add_header X-Cache-Status $upstream_cache_status;

    ...
}

モバイル判定

WordPress のテーマとして、レスポンシブではなくサーバサイドでモバイルとの何か差分を作っている場合、キャッシュも分けないといけません。

WordPress では is_mobile() という関数が用意されていて、これをつかってモバイル判定ができるので、これに合わせて nginx も設定するとよさそうです。

vars.php in tags/4.9.5/src/wp-includes – WordPress Trac

今回はおおむね iPhone と Android だけという区別でいくので Mobile|Android という指定にしました。

set で変数を設定し proxy_cache_key にその変数を入れることで、キャッシュする内容をモバイルとそれ以外とで分けることができます。

キャッシュをしないとき

管理画面にログインしていたり、コメントフォームに投稿した名前が記録されていたりなど、ユーザのもっているクッキーに応じて、テーマ上で値が出し分けされる場合、その内容をキャッシュしてはいけません。
(ログインしていないのに管理バーが出ちゃうよ!)

comment_author_|wordpress_(?!test_cookie)|wp-postpass_ として示されるクッキーがあるかを判定すると良いみたいです。

ここもモバイル判定と同様に変数に入れます。

この変数は proxy_no_cache と proxy_cache_bypass で使います。
proxy_no_cache はレスポンスをキャッシュに保存しない設定で proxy_cache_bypass はリクエストに対してキャッシュを使ってレスポンスを送らない設定です。
どちらも 0 ではないときに有効になります。

キャッシュの有効期限

proxy_cache_valid を使うことで有効な時間を設定できます。

ステータスコードをスペース区切りで書き、その後ろに有効期限を記載します。使える単位は ms/s/m/h/d/M/y です。

Configuration file measurement units

ここでは、ステータスコードが 200 または 301 または 302 のときに 30 日の間保持するようにしています。

また any という記載はそれ以外のステータスコードすべてに対して適用されます。ここでは 10 分にしています。

キャッシュが使われたか確認する

$upstream_cache_status という変数を見ると、キャッシュを使ったかどうかがわかります。

Module ngx_http_upstream_module

値として、おおむね HIT か MISS 、期限切れの場合は EXPIRED 、キャッシュしない場合は BYPASS がそれぞれ HTTP ヘッダーに出ているはずです。それ以外はちょっとわかんないです。

nginx の再起動

設定した内容に問題ないかを確認し nginx の再起動やりロードをしないと設定が反映されません。

service nginx configtest
service nginx condrestart

このときに proxy_cache_path に設定したパスが存在しないと自動で作られることはないので、自分で作っておく必要があります。

キャッシュの削除

このままでは WordPress で新しい投稿をしてもキャッシュはが有効な間は、新しい投稿についてはキャッシュ上のコンテンツからたどることはできません。
ここでは 30 日が有効期限なので、月 1 回しか更新することができなくなってしまいます。

手動でキャッシュを消す

proxy_cache_path に設定したパスにキャッシュファイルがもりもり作られるので、そのファイル群を消せばキャッシュはなかったことになります。

rm -rf なんかでどばーっと消すと良いでしょう。

毎回手動でキャッシュを消すのも面倒なので、 WordPress から消せるようにします。

WordPress で投稿を保存した時に自動でキャッシュを消す

// functions.php
function clean_nginx_cache($postId) {
    exec('rm -rf /tmp/nginx/cache/*');
    file_get_contents(get_permalink($postId));
}
add_action('save_post', 'clean_nginx_cache');

手動でやるものを WordPress のアクションフックに設定して自動化したものです。
投稿を保存した時に自動でキャッシュを消し、なんならその投稿のキャッシュを作成することができます。

プラグイン API/アクションフック一覧/save post - WordPress Codex 日本語版

 

プラグインもありそうなので、そういうのを使ってもよいと思います。

“nginx cache” の検索結果 — WordPress プラグイン

nginx の設定でなんとかする

今回の環境では利用できなかったのですが(モジュールがないっぽい…?) proxy_cache_purge を使うと nginx 上でキャッシュ削除の設定ができるようになります。

Module ngx_http_proxy_module

おわりに

nginx + WordPress の環境で proxy_cache を有効にした際の覚書です。

有効にした結果ですが、レスポンスに 500ms 掛かっていたページが 50 - 100ms ほどに収まるようになりました。あまりプラグインなども入れていない簡素なテーマを使っているところなのですが WordPress って結構時間かかるんだなあ…。

今回のものはわりとシンプルなテーマ、プラグインの状況だったので設定することや考えることが少なかったのですが、マルチサイトやプラグインによる大幅な機能変更がついてくると、もっといろいろなことを考えないとうまくキャッシュすることが出来なくなりそうな予感がしています。

特に他人の情報が見えちゃうよ!とかはキャッシュあるあるかつ激ヤバ案件なので、そういった時には入念にテストも重ねて回避したいですね~。

Google App Script を使って Bitbucket Pipelines の動いている状況を見える化した

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

某所で Bitbucket Cloud を使っていて、そのうえで動く CI 、 Bitbucket Pipelines を利用しています。

Bitbucket Pipelines | Atlassian

CI 時間が月 500 分を超えるまでは無料で利用できるのですが(すごい)、あっちこっちのリポジトリで使い始めたり、プッシュ頻度の高いリポジトリで使ったり
あるいは コード量が多くて CI の時間がかかるようなリポジトリ、そんなところで使い始めると途端に課金額がマッハになってしまいます
(ブランチを絞る、手動だけなどしてもいいけどそういうことじゃない)

いきなり XXXXX ドルの請求です!なんて届くと超ビビってしまうので、
毎月どれくらいの請求額になりそうか、過去はどれくらいだったのかを記録して通知するものを スプレッドシート + GAS で作ってみました。

なぜスプレッドシートと GAS なのか

同某所では GSuite を利用しているので、 Google Drive を組織的に利用できます。
GAS を使うことで、定期的にプログラムを実行することができるうえ、他の外部の何か(例えばサーバなど)について管理することが不要になります。
管理コストが少なく、かつ、スプレッドシートを基軸として、今後の分析にも使えるのでは?と考えると、スプレッドシート最高だなぁという結論になりました。

何も管理する必要なくオンラインにエクセルがいて、マクロの自動実行が出来るよって考えたらやばいと思うんだけど。

Google スプレッドシート - オンラインでスプレッドシートを作成、編集できる無料サービス

Apps Script | Google Developers
Spreadsheet Service | Apps Script | Google Developers

どのように Pipelines の利用状況を集めるか

Pipelines 、というか Bitbucket にはプログラム的に操作やデータ取得を行えるよう API が提供されています。
この中に Pipelines に関するものがあるので、これを利用します。

Bitbucket API

リポジトリ一覧を取得 → 各リポジトリに対して Pipelines の利用状況を確認 というながれで情報を集めます。

リポジトリ一覧 API : Bitbucket API
Pipelines状況 API : Bitbucket API

GAS から API を利用してデータを集める

というわけで出来上がったものがこちらです。

var BitbucketApi = {};

BitbucketApi.getAuth = function () {
  var options = {};
  options.headers = {};
  options.headers.Authorization = "Basic " + "ここに base64(ID:PW) したものを入れる";
  
  return options;
};

BitbucketApi.fetch = function (api) {
  var url = "https://api.bitbucket.org" + api;
  try {
    var rawResponse = UrlFetchApp.fetch(url, BitbucketApi.getAuth());
  } catch (ex) {
    Logger.log(ex);
    return null;
  }
  return JSON.parse(rawResponse.getContentText());
};


var Util = {};

Util.each = function(ary, cb) {
  for (var i=0, m=ary.length; i<m; i++) {
    if(cb(ary[i], i) === false) break;
  }
};

Util.getSheetByName = function (name) {
  if (Util.SpreadSheet == null) {
    Util.SpreadSheet = SpreadsheetApp.getActive();
  }
  
  return Util.SpreadSheet.getSheetByName(name);
};

Util.vlookup = function (searchValue, targetValues, column) {
  var result = null;
  Util.each(targetValues, function(row) {
    if (row[0] === searchValue) {
      result = row[column];
      return false;
    }
  });
  
  return result;
};

Util.yesterDay = function(date) {
  date = date || new Date();
  date.setDate(date.getDate() - 1);
  return date;
};

Util.startDay = function(date) {
  date = date || new Date();
  var d = new Date(date);
  d.setHours(0);
  d.setMinutes(0);
  d.setSeconds(0);
  return d;
};

Util.endDay = function(date) {
  date = date || new Date();
  var d = new Date(date);
  d.setHours(23);
  d.setMinutes(59);
  d.setSeconds(59);
  return d;
};


// reposシート、リポジトリチェック済みフラグのカラム番号
var REPOSITORY_CHECK_COLUMN = 4;


// リポジトリのチェックフラグの初期化
function initPipelinesDuration() {
  var repoSheet = Util.getSheetByName("repos");
  var repoRange = repoSheet.getRange(2, REPOSITORY_CHECK_COLUMN, repoSheet.getMaxRows() - 1, 1);
  repoRange.setValue(0);
}


// pipelinesシートの初期化
function resetPipelinesDuration() {
  Logger.log('WARNING!! Reset record durations!!  exit script.');
  return; // warning, danger.
  
  var sheet = Util.getSheetByName("pipelines");
  if (sheet.getMaxRows() > 1) {
    sheet.deleteRows(2, sheet.getMaxRows() - 1);
  }
  sheet.getRange(1, 1, sheet.getMaxRows(), sheet.getMaxColumns()).setValue("");
  sheet.getRange(1, 1, 1, 4).setValues([["repo", "build_number", "build_seconds", "completed"]]);
}


// リポジトリ一覧の同期
function syncRepositories() {
  // reposシートをまっさらにする
  var sheet = Util.getSheetByName("repos");
  if (sheet.getMaxRows() > 1) {
    sheet.deleteRows(2, sheet.getMaxRows() - 1);
  }
  sheet.getRange(1, 1, 1, 4).setValues([["repo", "url", "project", "status"]]);

  // BitbucketAPIを使ってリポジトリ名一覧をシートに移す
  var page = 1;
  while (true) {
    var repositories = BitbucketApi.fetch("/2.0/repositories/チーム名/?sort=-created_on&pagelen=100&page=" + page);
    if (repositories == null || repositories.values == null || repositories.values.length == 0) {
      break;
    }
    Util.each(repositories.values, function(repo) {
      sheet.appendRow([
        repo.slug,
        repo.links.html.href,
        repo.project.name
      ]);
    });
        
    page++;
  }  
}

// pipelinesシートの更新
function syncPipelinesDuration() {
  var sheet =  Util.getSheetByName("pipelines");
  var pipelineSheetRows = sheet.getLastRow();
  var todayStart = Util.startDay(Util.yesterDay());
  var todayEnd = Util.endDay(Util.yesterDay());
  
  var repoSheet = Util.getSheetByName("repos");
  var repos = repoSheet.getSheetValues(2, 1, repoSheet.getMaxRows(), 3);
  
  // 既にシート上にそのビルドの記録があるかチェック
  var isExistsBuild = function(repo, num) {
    var find = false;
    Util.each(sheet.getSheetValues(2, 1, sheet.getLastRow(), 2), function(row) {
      if (row[0] + row[1] === repo + num) {
        find = true;
        return false;
      }
    });
    
    return find;
  };
  
  Util.each(repos, function(repo, rowIndex) {
    if (repo[0] == "" || repo[2] == "1") {
      Logger.log(repo[0] + " : skip");
      return;
    }
    Logger.log(repo[0] + " : sync");
      
    repoSheet.getRange(rowIndex + 1, REPOSITORY_CHECK_COLUMN).setValue(1);
    
    var page = 1;
    while(true) {
      Logger.log(repo[0] + " : sync : page " + page);
      var response = BitbucketApi.fetch("/2.0/repositories/チーム名/" + repo[0] + "/pipelines/?sort=-created_on&pagelen=100&page=" + page);
      if (response == null || response.values.length == 0) {
        break;
      }
      var breakFlag = false;
      
      Util.each(response.values, function(item) {
        // 完了していないビルドはスキップ
        if (item.completed_on == undefined) {
          return;
        }
        var formattedCompletedOn = item.completed_on.replace(/T.+Z/, "");
        var completedTime = new Date(formattedCompletedOn);
        if (completedTime.getTime() > todayEnd.getTime()) {
          return;
        }
        if (completedTime.getTime() < todayStart.getTime()) {
          breakFlag = true;
          return false;
        }
        
        if (isExistsBuild(item.repository.name, item.build_number)) {
          return;
        }
        
        sheet.appendRow([
          item.repository.name,
          item.build_number,
          item.build_seconds_used,
          completedTime.toLocaleDateString(),
          '=MONTH(D' + pipelineSheetRows + ')=month(now())',
        ]);
        pipelineSheetRows++;;
      });
      
      if (breakFlag) {
        break;
      }
      
      page++;
    }
  });
};

認証周りだけちょっとアレなので、なんとかしたいなあとか思いつつも直打ち。

実装としては次の 2 つの処理に分かれます。

  • リポジトリ一覧の同期

    • 1 日 1 回 午前中 に実行
  • Pipelines 状況の取得

    • 1 日 1 回 お昼ごろ に実行
    • 1 日 1 回 おやつごろ に実行
    • 1 日 1 回 夕方ごろ に実行

リアルタイムに動かすことが難しいので、一日一回動かして、昨日分のデータを取得するようにしています。
Pipelies は 1 度に取りきることが難しいので、何回か動かします。進捗はシートに記録されるので、再度取得することはありません。

あとは、これだけだとシートがなくて動かないので、空っぽのシートを用意したら使えるはず。

  • pipelines
  • repos

収集してからのこと、集計と通知

実行するとこんな感じに Pipelines のデータが収集できるので、ここからスプレッドシートの機能で集計していきます。

ピボットテーブルを使って、当月の、各リポジトリの、実行時間(秒)を合計します。

こんな具合で設定しています。

ここから更に、少々強引な関数を組んで、このデータを参照して日毎の実行時間(分)を算出し、月単位でどれくらいの実行時間になりそうかをざっくり予測できます。

ちなみに使った関数はこんなものです。

データの参照 ( B1 )
=index(aggregate!A1:AA20)


ビルド数の抽出 ( C24 ~ xx26 )
=COUNTIF(pipelines!$D:$D,C1)


総計の抽出行 ( C25 ~ xx26 )
=if(C1="", 0/0, if(C1<>"総計", INDEX(C1:C20, LOOKUP("総計",$B1:$B20,$A$1:$A$20), 1), 0/0))


総計の分 ( C26 ~ xx26 )
=iferror(C25/60, "")


今月予測(ビルド分) ( C27 )
=average(C26:AB26)*22


今月予測(課金額) ( C28 )
=round(((C27-500)/1000)/10*10)*10

で、このときの C28 を UrlFetch で Slack に送るよう、トリガーで日々実行しています。

var SlackApi = {};
SlackApi.config = {};
SlackApi.config.webhook = "";
SlackApi.config.testMode = false;

SlackApi.sendMessage = function (text) {
  var payload = {
    text: text
  };
  
  var options = {
    method: 'post',
    payload: JSON.stringify(payload),
  };
  
  if (SlackApi.config.testMode) {
    Logger.log(SlackApi.config.webhook);
    Logger.log(options);
  } else {
    return UrlFetchApp.fetch(SlackApi.config.webhook, options);
  }
};


function sendSlack() {
  SlackApi.config.webhook = "https://hooks.slack.com/services/....";

  var sheet = Util.getSheetByName("aggregate");
  var usages = sheet.getSheetValues(2, 1, sheet.getLastRow(), sheet.getLastColumn());

  var resultMessage = [];
  resultMessage.push("> *Pipelines利用状況* (" + (new Date()) + ")");
  resultMessage.push("```");
  Util.each(usages, function(usage) {
    if (usage[0] == "") return;
    resultMessage.push(usage[0] + ": " + usage[sheet.getLastColumn() - 1] + "sec");
  });
  resultMessage.push("");
  
  try {
    var paceSheet = Util.getSheetByName("課金ペースをみたい");
    var paces = paceSheet.getSheetValues(27, 2, 2, 2);
    resultMessage.push(paces[0][0] + ": " + paces[0][1]);
    resultMessage.push(paces[1][0] + ": " + paces[1][1]);
    resultMessage.push("");
  } catch (ex) {
    Logger.log(ex);
  }

  resultMessage.push("詳細はここ> https://docs.google.com/spreadsheets/....");
  resultMessage.push("```");
  
  SlackApi.sendMessage(resultMessage.join("\n"));
}

これでいきなり多額の請求が来ることに怯えることが少なくなりました。遅くても 1 日で oops! なことに気づけるはずです。

モダンな xhprof = tideways

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

PHP で書かれた、パフォーマンスがクソほど悪い web サイトの様子を見ることになったので、どこに時間使っとんねん!を調べようと思い xhprof 入れるぞ!!と準備しようとしたら PHP7 以降はサポートしないらしい。
FB 社が HHVM に移行したから扱わなくなったのかな?わからん。

調べたら PHP7 対応の更新を独自に追加した xhprof と、 tideways という xhprof 互換のプロファイリングツールがあるらしいので tideways を使ってみる話。

XHProf fork with PHP 7.0, 7.1 and 7.2 support | Tideways

tideways エクステンションの準備

ここからエクステンションがダウンロードできる。

tideways/php-xhprof-extension: Modern XHProf compatible PHP Profiler for PHP 7

ちなみにここでタイトル回収なんですけど、こう説明書きされている。

Modern XHProf compatible PHP Profiler for PHP 7 https://tideways.io

 

Windowsの場合はAppVeyorのアーティファクトからになる。Github からもリンクがあるけど、ここ。

php-profiler-extension master.251 - AppVeyor

ここから自分の PHP バージョンを選んで、CI 結果の中から Artifacts があるので、そこに dll が入っている zip がある。

 

あとは環境を問わず、エクステンションを既存の別のエクステンションと同じディレクトリに入れて php.ini で extension=... を記載を追加して読み込めば良い。

こいつは Windows でやってる。

php -m すると今読み込んでいるエクステンションが確認できるので、このときエラーが出ずに tideways の記載が見えれば準備 OK

注意事項として tideways のツール群を使わない(xhprof相当のことだけローカルでやりたい。tidewaysのインフラを使わない)なら
公式サイトでアナウンスされている yum や apt-get を使ったインストールはしないこと。
そっちだとうまくできないっぽい。

Mission control center for PHP application performance | Tideways

ただ tideways のツール群にのっかると、プロファイリングやアラート通知が出来るよ!と書いてあって非常に便利そう。
ツール群を入れたら自動でプロファイリングして集計して可視化までしてくれる。超便利やん…

tideways の xhprof 互換エクステンションを使ってプロファイリングの実施

Github にも書いてあるが、ざっくりとはこういう形。

tideways_xhprof_enable();

start_my_application();

$data = tideways_xhprof_disable();
file_put_contents(__DIR__ . "/1.xhprof", serialize($data));

これで記載したファイルと同じディレクトリに 1.xhprof というファイルが出来上がる。

Laravel や CakePHP なら一番最初に呼ばれる index.php をまるっと囲ってしまうと何も考えずに「どこのページが重たいかな~~フッフ~ン」と楽できる。
楽できる一方で、全ての処理について出てしまうので「特定のページが重いんだよなあ~~」みたいなときには使いにくい。
そのときは個別にやっていくと良さげ。

プロファイリング結果の確認

出力された結果は xhprof と互換性があるので、xhprof を可視化するツールでそのまま見れる。
例えば xhgui がいろいろできて便利そうに見える。

perftools/xhgui: A graphical interface for XHProf data built on MongoDB

いろいろ必要なのがあるなあとか、変なところで詰まるのもなあ、と思い、今回はとりあえず結果を見たかっただけなので xhprof に付属する xhprof_html を使った。
xhgui の話はまた今度にする。

 

ただ、xhprof_html は xhprof の設定が前提となるので、それそのままだと動かない(主にディレクトリ周り)ので、そのあたりを少しいじった。こちらを使いたい方はどうぞ。

sters/xhprof-html: xhprof (or tideways) visualize html tool from xhprof repo.

これを使うとこんな感じの Web ページが見えるようになるので、これでおしまい。

この画面から、どのメソッドが何回呼ばれてどれくらい時間がかかっているかという、PHP アプリケーションのボトルネックが丸見えになってくるので、パフォーマンスチューニングドンドンやっていける。

余談) PHP7 対応された xhprof

これは Travis でぐるぐる CI も回してる。 PHP 7.0 と 7.1 で CI されてるので、そこは確実に使えるっぽい。

yaoguais/phpng-xhprof: upgrade xhprof extension to PHP7

使い方は README にも書いてあるとおりで xhprof とまったく同じでいい。エクステンション入れて xhprof_enable() したら良い。
結果は xhprof_disable() すると受け取れるので、それを serialize() して保存すると各種ツールで見れる。

textlint のルールを作ってみた

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

先日、textlint というめちゃ便利な text を lint するツールの追加ルールを作ってみた話。ちょっと前にやったんだけど記事にしかけたまま止まってたのでいい加減に書いた次第。

作ったルール

sters/textlint-rule-a3rt-proofreading: [Unofficial] textlint rule that using A3RT Proofreading API.
textlint-rule-a3rt-proofreading - npm

A3RT という機械学習 API サービスの文章校正を textlint から利用できるようにしたもの。
文章校正 API は万能なものではなく、特定ドメインに特化しているので、こういった文章にも上手く適合してくれないこともあるが、とりあえず textlint から呼べるようにしてみるか、というくらいの気持ちでやった。

textlint とは

textlint/textlint: The pluggable natural language linter for text and markdown.

text を lint するツール。いろんな人の作ったいろんなルールを組み合わせて、最強の文章を作っていけるようになるはず。

prh と web+db ルールを入れるとたのしい。

textlint-rule/textlint-rule-prh: textlint rule for prh.

rules/WEB+DB_PRESS.yml at master · prh/rules

A3RT

A3RT
ABOUT|A3RT

A3RT(アート)は「ANALYTICS & ARTIFICIAL INTELLIGENCE API VIA RECRUIT TECHNOLOGIES」の略称です。
...
当初はA3RTについても、リクルートグループ内に限って導入してきましたが、以下のような理由からこのたびグループ外への提供を決意しました。

ちなみにこれ超重要なんですけど、私自身はリクルート社とは関係ないです。
今回作った textlint ルールは勝手にやったものなので何か問題があれば削除するなり対応するので連絡ください。

textlint ルールの作り方

textlint の中にツールがあるのでそれを使うとスケルトンが出来上がります(すごい)

参考: textlintのルールを簡単に作り始めることができるツールを作りました | Web Scratch

基本セットを作れるやつ
textlint/create-textlint-rule: Create textlint rule project with no configuration.

ビルドしたりテストしたりを出来る便利な子
textlint/textlint-scripts: textlint npm-run-scripts CLI help to create textlint rule.

textlint-scripts が超便利で、基本的にはテストの云々を何も気にしないで、ルールのロジックに集中してテストを書けたり。npm へ上げるときに、何も気にせず babel してくれたりするので、とりあえず ES2015+ なコードを書くと動いてくれるのですごい楽。

あとはドキュメントを見たり、データ構造を見たり、他のルールを見たりして、実装する。実装にあたってはヘルパーライブラリがあるので、これも使っていくと AST をこねこねするような込み入ったルールも簡単に書けそう。

ルールの作り方(データ構造とか)
textlint/rule.md at master · textlint/textlint

ヘルパーライブラリ
textlint/textlint-rule-helper: This is helper library for creating textlint rule.

おわり

独自のルール作るのが思いのほか簡単だったので、何か思うところがあれば作ってみるとよいのではー!
特に正規表現ベースのルールとかは作るのがめちゃんこ楽なのでーーーーー!!!

あいまいな VLOOKUP 関数。レーベンシュタイン距離を添えて。

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

完全に一致するものを探す VLOOKUP 関数ではなく、ある程度あいまいな VLOOKUP 関数がほしい状況になったので、やってみた件。

レーベンシュタイン距離を使うとお手軽にあいまい検索っぽいことができそうなので、これを利用する。
すげーざっくり言うと 2 つの文字列を比べて、片方から見た時に何文字変更したら一緒になりますか?ってもの。
レーベンシュタイン距離 - Wikipedia

Google Spreadsheet で使いたかったので Google App Script なのだけど VBA に移したら Excel でも同様に利用できるはず。Excel だと配列を返して出力はできないが。

function fuzzyVlookup(targetList, searchItem, showDistance, showNth) {
  showDistance = showDistance || 0;
  showNth = showNth || 1;
  
  searchItem = searchItem.toString().split("");
  
  var results = targetList.map(function(targetItem, idx) {
    targetItem = targetItem.toString().split("");
    
    if(targetItem.join("") == "") {
      return null;
    }

    if(searchItem.join("") == targetItem.join("")) {
      return {distance:0, idx:idx};
    }
    
    var leven = []; 
    for (var i = 0; i <= targetItem.length; i++) { 
      leven[i] = []; 
      leven[i][0] = i; 
    } 
    for (var i = 0; i <= searchItem.length; i++) { 
      leven[0][i] = i; 
    } 

    for (var i = 1; i <= targetItem.length; i++ ) { 
      for (var j = 1; j <= searchItem.length; j++ ) { 
        var cost = targetItem[i - 1] == searchItem[j - 1] ? 0 : 1; 
        
        leven[i][j] = Math.min(
          leven[i - 1][j] + 1,
          leven[i][j - 1] + 1,
          leven[i - 1][j - 1] + cost
        ); 
      }
    }
    
    return {
      distance: leven[targetItem.length][searchItem.length],
      idx: idx,
    };
    
  }).filter(function(x) {
    return x !== null;
  }).sort(function(a, b) {
    return a.distance - b.distance;
  });
  
  if (results.length === 0) {
    throw new Error("要素が見つかりませんでした");
  }
  
  results = results.slice(0, showNth).map(function(x){
    var str = targetList[x.idx][0];
    if (showDistance === 1) {
      str += " dist=" + x.distance;
    }
    return str;
  });
  
  return [results];
}

↓こんな感じで使える。良好。

1 2 3 4 5 6 7 8 9 10 11 12 13