ushumpei’s blog

生活で気になったことを随時調べて書いていきます。

serverless の設定で簡単なロジックを含める

stage とかに応じて設定を変える方法があったのでいじってみます、${opt:stage} とかよりもっと複雑なことしたくなったときに使えると思います: https://www.serverless.com/framework/docs/providers/aws/guide/variables/#exporting-a-function

なんとなく TypeScript でテンプレート作成しました

sls create -t aws-nodejs-typescript -p serverless-demo
import type { AWS } from '@serverless/typescript';

import hello from '@functions/hello';

const serverlessConfiguration: AWS = {
  ...
  custom: {
    hoge: "${file(./hoge.js)}",
    ...
  },
};

module.exports = serverlessConfiguration;

hoge: "${file(./hoge.js)}"hoge.js に定義した関数を呼び出してくれるらしい

パラメータはどんな感じか見たいのでこんな感じの関数を書いた

module.exports = async (p) => {
  console.log(p)

  return {}
}

serverless print したらこんなの出てきた

{
  options: [Object: null prototype] {
    format: null,
    path: null,
    transform: null,
    region: null,
    'aws-profile': null,
    app: null,
    org: null,
    'use-local-credentials': null,
    config: null,
    stage: null,
    param: null,
    help: null,
    version: null,
    verbose: null,
    debug: null
  },
  resolveConfigurationProperty: [AsyncFunction: resolveConfigurationProperty],
  resolveVariable: [AsyncFunction: resolveVariable]
}

hoge: "${file(./hoge.js)}"hoge: {} として解決されていた

感想

javascript の ArrayBuffer

なんとなく javascript の ArrayBuffer 触っている。データをストリームで処理することが自分は好きなのかもしれないって最近思って、テキストは特にすることがないのでバイトデータ、という感じで。

とりあえず jpeg からサイズ情報が取れる程度のパーサーを実装してみた。GPS は取れるかどうか試していない。サーバでもブラウザでも使えるように tsconfig を調整したりやや勉強になった。(TypeScript で書いている時に import ... from './xxx/xxx.js' みたいに書くの新鮮な違和感だった)

github.com

png, zip とか pdf もちょっとずつ処理できるようにしたいなと思ったりしている。これらがなんかプロダクトにつながってくれればいいけど、今のところあまり考えられていない。

サーバとブラウザでも使えるようにした理由として、サービスワーカー上でも使えて、必要に応じてサーバでも処理できるようなものを書きたいと思ったから (fetch イベントを状況に応じて切り替える感じ)。電波の貧弱な国に少しいたのでブラウザでできることはブラウザでやってしまいたいというのが理由の背景となる体験だと思う。ちょっとファイルを加工するくらいの処理ならブラウザでやってしまった方が費用の面でも安くなるだろうし。

他のデータ形式追加していくのもいいけど、プロダクトに繋がるものがモチベーション保てていいなとも思う。RFB protocol とかも楽しいかもしれないけど、ちょっと昔書いて挫折した記憶がある、状態遷移を上手く扱えなかった感じ (A メッセージを受け取ったら B モードに移行して C メッセージを待つ、みたいな部分が雑だった)。

とりあえずプロトコルとかデータ形式を羅列してみて、面白そうで何か作れそうなやつを考えるのがいいかもしれない。

ブログを放置しすぎてしまっているのが気になって久々に書いてみた。

Deno で簡単な重複行削除スクリプトを書いた

ファイルの重複行削除がしたかったのでスクリプトを書こうと思ったのですが、せっかくだし Deno で書いてみることにしました。

https://github.com/ushumpei/scripts/blob/main/remove_duplicate_lines.ts

import { iter } from "https://deno.land/std@0.93.0/io/util.ts";

const N = Deno.env.get("NEWLINE") || "\n";
export type M = {
  b: boolean;
  d: string[];
  h: { [k: string]: boolean };
};

const i = Deno.args[0];
const o = Deno.args[1];

const f: Deno.File = Deno.openSync(i, { read: true });
const m: M = { d: [], h: {}, b: false };
const t: string = await Deno.makeTempFile();

let r = "";
for await (const ck of iter(f)) {
  const ls = (r + new TextDecoder().decode(ck)).split(N);
  r = ls[ls.length - 1];

  const ot = ls
    .slice(0, -1)
    .reduce((_m: M, l: string) => {
      if (!_m.h[l]) {
        _m.h[l] = true;
        _m.d.push(l);
      }
      return _m;
    }, m)
    .d.join(N);
  if (ot.length === 0) continue;

  const of = new TextEncoder().encode(m.b ? N + ot : ot);
  Deno.writeFileSync(t, of, { append: true });

  m.d = [];
  m.b = true;
}
f.close();

Deno.copyFileSync(t, o);

github にあげていて、こんな感じで URL 指定でも実行できます。

$ deno run --allow-env --allow-read --allow-write \
> https://raw.githubusercontent.com/ushumpei/scripts/main/remove_duplicate_lines.ts \
> 入力ファイル名 \
> 出力ファイル名

感想

  • groovy のようにライブラリのインポート含めてスクリプトが一枚のファイルで完結するので、結構楽でいいです。しかも URL 指定でインポートできるのでより手軽。なので書くときにインポートするものをちゃんと精査しないといけないと思います。 --allow-net などオプションで権限が指定できるので、その辺もしっかり吟味していきたいです。(権限つけ忘れた時のエラー文がわかりやすいのも良い感じ)
  • 書いたスクリプトで宣言している変数名は一文字とかばかりなのですが、なんかテンション上がって極力短くしてみました。楽しかった。
  • 環境変数による改行コードの指定は未テストで、いらないんじゃないかなーと思っています。--allow-env も消せるしそうしたい気がしてきた。
  • メモリに一気に乗せないように書いたつもりだけどどうなんだろう。
    • 既出行を格納しているオブジェクトが巨大になって死ぬとかありそう。
    • あと copyFileSync は中身をみていないけどちょろちょろコピーしてくれるのだろうか?
    • あとこれ TransformStream で描き直したい。
  • Denovscode 拡張入れた後上手く設定できてなくて cannot find name Deno とか言われてたけどコマンドパレットから Deno: Initialize Workspace Configuration 実行したらなんか上手くいった。
  • 無限インポートループとかどうなるんだろうか。
  • https://doc.deno.land/builtin/stable 見て書いた。
  • Deno Deploy はまだちゃんと触っていない、リクエスト処理は fetch イベントのハンドラー書いてやるみたいだけど、ローカルだと発火しなくてちょっと手間取った、なんか公開してくださっているライブラリ入れたら動いた。https://deno.land/x/fetch_event_adapter/listen.ts

WordPress で独自の rss フィードを設定する

意外に簡単だったけど忘れそうなのでメモ。 functions.php に以下を記述

<?php

add_action('init', function() {
    add_feed('custom', function() {
        header('Content-Type: application/rss+xml');
        include_once 'custom_feed.php';
    });
});

デフォルト?だと サイトURL?feed=customcustom_feed.php の中身が返ってくるようになる。でも WordPress から「ちゃんとした xml じゃない!」みたいな怒られ方するので、custom_feed.php を作るのがまた一苦労な感じだと思いました。正しい feed 生成方法とかあるのだろうか。

感想

WordPress いじるときは wp-env と言う WordPress ローカル環境を立ち上げてくれる npm ライブラリを使っていてなかなか快適です。思い出したけど Feedly 整理しなければいけない。

Java でストリームの末尾数行を捨てる

末尾の行を捨てるためには一旦ファイルを全部読まなきゃいけないかと思ったけど、捨てたい行数を先読みしておけば良いという感じでした。

import java.io.*;
import java.util.LinkedList;
import java.util.Queue;

// ファイルの末尾数行捨てる BufferedReader
class TailIgnoringBufferedReader extends Reader {
    private final BufferedReader _reader;
    private final int num;

    Queue<String> queue = new LinkedList<>();

    TailIgnoringBufferedReader(Reader in, int num) {
        this._reader = new BufferedReader(in);
        this.num = num;
    }

    public String readLine() throws IOException {
        while (this.queue.size() <= this.num) {
            String line = this._reader.readLine();
            if (line == null) return null;
            this.queue.add(line);
        }
        return this.queue.poll();
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        return this._reader.read();
    }

    @Override
    public void close() throws IOException {
        this._reader.close();
    }
}

( lines とかは未実装。BufferedReader と名付けていいかは微妙なところかも。)

こんな感じで使えます

InputStream data = new ByteArrayInputStream("1\n2\n3\n4\n5\n6\n7\n8\n9".getBytes());
try (TailIgnoringBufferedReader reader = new TailIgnoringBufferedReader(new InputStreamReader(data), 5)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

先頭も捨てられるようにすれば任意の切り出し方ができるようになる

感想

InputStream 系のコードは書いていて楽しい

read とかでも対応するにはどうするべきなんだろうか?(先読みは同じで、 currentLine みたいなインスタンス変数持ってそこから返していく、空になったら Queue からロードする感じかな)

WordPress のテーマで、依存している Plugin をインストールしてもらう

必要な plugin がある場合に、通知を表示してインストールしてもらう

  1. class 作る (名前空間としてだけ使っている、作法はわかっていない)
  2. functions.phprequire_once して呼び出す

コード

required_plugins.php

<?php

class RequiredPlugins
{
    public static function setup($plugins)
    {
        add_action('init', function () use ($plugins) {
            if (!current_user_can('activate_plugins')) return;

            foreach ($plugins as $plugin) {
                if (self::is_plugin_active($plugin['key'])) continue;

                add_action('admin_notices', function () use ($plugin) {
                    $name = $plugin['name'];
                    $url = network_admin_url( "plugin-install.php?tab=search&type=term&s=${name}&plugin-search-input=Search+Plugins" );
                    echo "<div class=\"error\"><p>The <a href=\"${url}\">${name}</a> is required.</p></div>";
                });
            }
        });
    }

    public static function is_plugin_active($key)
    {
        include_once(ABSPATH . 'wp-admin/includes/plugin.php');
        return is_plugin_active("${key}/${key}.php");
    }
}

呼び出す時

functions.php

<?php
...
require_once 'required_plugins.php';
...
RequiredPlugins::setup([
    ['key' => 'import-users-from-csv', 'name' => 'Import Users From CSV']
]);

注意

  • ['key' => '...', 'name' => '...'] の形の要素の配列で、複数の通知を表示させる
  • keyプラグインディレクトリ名、wp-content/plugins 以下から確認してくる
  • name は何でもいいが、ユーザーがわかるようにする
  • is_plugin_active で一応プラグインが有効化されているかどうかの判定ができる

参考

How would you require and automatically download dependent plugins? - WordPress Development Stack Exchange

感想

  • IT お仕事、割と WordPress で解決することもあるので知っておくと便利かもしれない
  • 今のところ以下でやりくりしている
    • フックとフィルターを使ってイベント駆動で書く
    • ちょっとしたデータ永続化は wp_options に入れる
    • 本体やテーマやプラグインのコード読む
  • でも本体の運用はできればしたくないからテーマで提供するスタンス

Vue.js の slot と props

内容としては これ (Vue.js のガイドの「スロット」の「スコープ付きスロット」) を読んで理解できなかった自分向けのメモです。

Vue.js で slot を使ったコンポーネントの持つ値を、slot に差し込まれるコンポーネントに props として渡す方法を調べました。

slot を使ったコンポーネントHoge コンポーネント、差し込むコンポーネントFuga コンポーネントとすると、以下の感じでいけます。

Hoge コンポーネントslot の部分に <slot :hoge="hoge" /> みたいに渡したい値書いて、実際このコンポーネントを使うときに

<Hoge v-slot="{ hoge }">
  <Fuga :fuga="hoge" />
</Hoge>

のように v-slot という構文で hoge を取り出して子のコンポーネントに props として渡してあげればいけます。

要するに slot タグに露出させたい属性値書いておけば v-slot で取り出せる感じ。

サンプル

登場するコンポーネントは 3 つで、

になります。

blur コンポーネントがクリックされたことを、spark コンポーネントに伝え、点滅を開始してもらうという動作になります。

See the Pen Vue Slot Props by ushumpei (@ushumpei) on CodePen.

実用的では無い例です。自分が実際にこれを必要としたのは、タブのコンポーネントで、タブがアクティブになったら子コンポーネントに伝えてデータフェッチし直す、的なやつです。

感想

  • v3 で統一されたらしく v-slot が 2 つの意味で使われていることに気がつくのが遅かった。
  • slot タグに属性値書かないで v-slot で取り出そうとしてもエラー出ないのハマりそう。
  • slot の説明読むとき、親子コンポーネントだけじゃなくてそれらを組み合わせる第三のコンポーネントが存在することを忘れがち。
  • あと親子って言い方が自分にはわかりにくくて、slot タグ持っている方とか、差し込まれる方とかいいがち。

Node.js の stream.pipe のエラーハンドリング

stream.pipe の代わりにバージョン 10 で追加された stream.pipeline を使うと良さそうです。

stream.pipe では ここ に書かれているように、エラーハンドリングとしてストリームの後始末を書かなければいけないためです。

One important caveat is that if the Readable stream emits an error during processing, the Writable destination is not closed automatically. If an error occurs, it will be necessary to manually close each stream in order to prevent memory leaks.

あと pipe(...).on('error', ...).pipe(...).on('error', ...)... みたいに書かなきゃいけない気がするし。

確認コード

stream.pipeline でどのようなエラーハンドリングが行われるかをコードで確認しました。ファイルを読んで、変換をかまして、ファイルへ書き込むサンプルです。変換部分でエラーを任意に発生させられるようにし、

  1. エラーが発生しない時の挙動
  2. エラーが発生した時の挙動

の二つを確認します。

(動かすには入力用のファイル from.txt が必要になります)

const { Transform, pipeline } = require("stream");
const { createReadStream, createWriteStream } = require("fs");

// Readable ストリーム作成
const readable = createReadStream("from.txt", "utf8");

// Writable ストリーム作成
const writable = createWriteStream("to.txt", "utf8");

// Transform ストリーム作成 (何もしないけどエラー投げられるようにしたやつ)
const getTransform = (fail) =>
  new Transform({
    transform(data, _encoding, callback) {
      if (fail) {
        callback(new Error("fail"));
      }
      this.push(data);
      callback();
    },
  });

// [NOTE] ここでエラーの発生を任意に切り替える
// const transform = getTransform(true);
const transform = getTransform();

// イベントの発生をロギング。`i-th` は各ストリームに対応 (0-th: readable, 1-th: transform, 2-th: writable)
[readable, transform, writable].forEach((s, i) => {
  s.on("end", () => {
    console.log(`${i}-th stream:`, "end");
  });
  s.on("finish", () => {
    console.log(`${i}-th stream:`, "finish");
  });
  s.on("close", () => {
    console.log(`${i}-th stream:`, "close");
  });
});

// 入力 -> 変換 -> 出力
pipeline(readable, transform, writable, (e) => {
  if (e) {
    console.log(e);
    console.log(
      "エラーが起きた時は全てのストリームに close イベントが発行される"
    );
  } else {
    console.log(
      "エラーなく終了した時は全てのストリームにそれぞれの終了イベント、加えて入出力ストリームに close イベントが発行される"
    );
  }
});

1. エラーが発生しない時の挙動

0-th stream: end
1-th stream: finish
1-th stream: end
2-th stream: finish
エラーなく終了した時は全てのストリームにそれぞれの終了イベント、加えて入出力ストリームに close イベントが発行される
0-th stream: close
2-th stream: close

2. エラーが発生した時の挙動 ([NOTE] と書いてあるコメント行近くの transform 変数定義をコメントアウト/インして実行)

1-th stream: close
2-th stream: close
Error: fail
    at Transform.transform [as _transform] (/Users/ushumpei/node-stream/index.js:15:18)
    at Transform._read (_stream_transform.js:189:10)
    at Transform._write (_stream_transform.js:177:12)
    at doWrite (_stream_writable.js:428:12)
    at writeOrBuffer (_stream_writable.js:412:5)
    at Transform.Writable.write (_stream_writable.js:302:11)
    at ReadStream.ondata (_stream_readable.js:722:22)
    at ReadStream.emit (events.js:209:13)
    at addChunk (_stream_readable.js:305:12)
    at readableAddChunk (_stream_readable.js:282:13)
エラーが起きた時は全てのストリームに close イベントが発行される
0-th stream: close

各イベントは、

  • end: Readable ストリームが読まれ終わった時に発火
  • finish: Writable ストリームが書かれ終わった時に発火
  • close: ストリームとその使用しているリソースが閉じた時に発火 (でも発火しないストリームもあるらしい)

と言う感じか?

まとめ

確認できたこととしては以下

  • どちらのケースでも close が呼ばれて使用しているリソースを閉じている。
  • transform は DuplexReadable かつ Writable なので endfinish が呼ばれている。

感想

Transformclose されるのはどう言うことだ?と悩む。

あと、根本的な問題として、ストリーム閉じなくて良いケースを理解できていない。文字列から単なるメモリに乗っている Readable なストリーム作った場合は、閉じなくて良いのか?GC されるか?

公式ドキュメントに promisifypipeline を Promise 化した時の例も書いてあったので、async/await で書きたい時はそれを使う。

Node.js でオブジェクトの配列からストリームを作成する

多分知っておくべきこととして、ストリームはバージョン 12.3.0 でかなり変化があった。この記事は 11.15.0 で書いている。

Node でオブジェクトの配列からストリームを作る方法。

const { Readable } = require("stream");

const readable = Readable.from({ objectMode: true, read() {} })
オブジェクトの配列.map((o) => readable.push(o))
readable.push(null)

12.3.0 以降 は以下で良い。

const { Readable } = require("stream");

const readable = Readable.from(オブジェクトの配列)

ストリーム自体の使い方があまりわかっていない。

とりあえず動かす

エラー時、ストリームを頑張って閉じなければいけないと思う、 destroy() とか使う。

const { Readable, Transform } = require("stream");

// テストデータ作成
const testData = new Array(1000)
  .fill("")
  .map((_, i) => ({ id: i, val: Math.random() }));
testData.push(null)

// Readable ストリーム作成
const readable = new Readable({
  objectMode: true,
  read() {},
});

// Transform ストリーム作成
const stringifyTransform = new Transform({
  transform(data, _encoding, callback) {
    this.push(JSON.stringify(data));
    callback();
  },
  objectMode: true,
});

// この辺微妙
stringifyTransform.on("error", () => readable.destroy());
process.stdout.on("error", () => {
  readable.destroy();
  stringifyTransform.destroy();
});

// 入力 -> 変換 -> 出力
readable.pipe(stringifyTransform).pipe(process.stdout);

// 入力へデータを入れる
testData.forEach((o) => readable.push(o));

標準出力にオブジェクトの文字列が表示される。

ストリームの閉じる閉じないの話

ストリームが閉じているかどうか調べる方法がわからない。イベントを監視していればできるがもっと良い方法はないだろうか? (12.3.0 以降とかだとプロパティがある)

以下はストリームの挙動を確認するための、ストリームを標準出力へパイプするコード。失敗させたりしてイベント拾う。

const { Readable } = require("stream");

// Readable ストリーム作成
const readable = new Readable({
  objectMode: true,
  read() {},
});

console.log(0, readable.readableFlowing);
// => null

const pipe = readable.pipe(process.stdout);

console.log(1, readable.readableFlowing);
// => true

console.log(2, readable.push("ok\n"));
// => true
// 出力あり

console.log(3, readable.push({}));
// => true
// pipe 先の process.stdout がエラー起こして失敗

pipe.on("error", (e) => {
  console.log(4, readable.readableFlowing);
  // => false

  console.log(5, readable.push("ok\n"));
  // => true
  // ただ pipe が切れているので出力はない

  readable.destroy();
  // readable を切る
});

readable.on("close", (e) => {
  console.log(6, "close"); // destroy() で close が発生する。pipe 先のエラーでは切れたりしないようだ
});

出力

0 null
1 true
2 true
3 true
ok
4 false
5 true
6 'close'
  • readableFlowing: パイプされているかどうか。最初 (0) では繋がっていないので null。繋がると (1) true、エラー発生後 (4) は切れてて false
  • pipeerror を拾うようにして、その中で各ストリームを切っていけば良いっぽい。

感想

API ドキュメントのストリーム部分 ちゃんと読んだほうがいいのだろうな、と思いました。英語読むのちょっと気合がいる。

疑問たくさんある

  • エラー時に transform も切る必要があるだろうか?
  • 各種イベントの正しい使い方。error 起きた時に閉じるとかちゃんとしなきゃダメだがあまり整理できてない。finish? end?
  • ストリーム作成時の read() {}pipe だと不要?実装してみる?
  • TransformReadableWritable を継承しているらしいが Writable っぽさが見えてない。
  • gzipTransform の実装どんなだ

fetchbody でストリーム取得できるから画像を canvas に書き込むとか楽しそう。何に使えるかはわからない。加工とかしてみる?RGB から一色なくすとかはできそう。ただそれは何のために?

ストリーム Java でしか使ったことがなかった

GZIPOutputStream と ByteArrayOutputStream と try with resources

Javatry with resources は途中で return してもリソースの close をしてくれるけど、なんか間違えた。

GZIPOutputStreamByteArrayOutputStream を使って以下のような、データを圧縮してバイト列にして返す処理を書いていました。( InputStream の部分は実際は外部のファイルのストリームとかになると思います)

String input = "hogehogehoge";
InputStream in = new ByteArrayInputStream(input.getBytes());

try (
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(out)))
) {

  String line;
  while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.newLine();
  }

  return out.toByteArray();

} catch (IOException e) {
  e.printStackTrace();
  throw new RuntimeException(e);
}

すると次の例外が発生。

java.io.EOFException: Unexpected end of ZLIB input stream

これは GZIPOutputStream を閉じていないときに出る例外で、「 try with resources だから閉じられるのでは?」と思いましたが、 toByteArray しているタイミングはまだ処理が try を抜けていないので閉じられていなかったようです。

以下のように変更して解決しました。バイト列を取得するのは try を抜けてから行うようにしました。

String input = "hogehogehoge";
InputStream in = new ByteArrayInputStream(input.getBytes());

ByteArrayOutputStream out = new ByteArrayOutputStream();

try (
  BufferedReader reader = new BufferedReader(new InputStreamReader(in));
  BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new GZIPOutputStream(out)))
) {

  String line;
  while ((line = reader.readLine()) != null) {
    writer.write(line);
    writer.newLine();
  }

} catch (IOException e) {
  e.printStackTrace();
  throw new RuntimeException(e);
}
return out.toByteArray();

感想

すぐ終わると思って書いたコードが動かなくて夜が明けてしまった。

最近景気が悪いので、仕事がなくなったら各地のお祭りを巡る旅とかしたい。

修了証明書

Certificate of Completion

スライドとかでよかった気がするけど、なんとなく html で修了証明書を書いてみました。css は無限に時間を溶かすと思います。

See the Pen Certificate of Completion by ushumpei (@ushumpei) on CodePen.

MacChrome でしか確認していない。もしかしたら他の環境でフォントとかダメかもしれないです。

COURSE DETAILS (リボンの下の細かい文字) が短すぎると良い感じの形にならないです。三文くらいあると印刷した時に A4 サイズにうまくおさまる。

参考

ありがとうございました。

リボン

saruwakakun.com

リボンのコード、border で三角形を作るやつの応用で白い三角形を使って切り口を表現する。

mrtc.jp

画像使わないと縁取りかっこよくならないかと思ったけど、radial-gradientlinear-gradient を使って丸みを出している。ただコードちゃんと追えていない。

感想

効力のない、お気持ちのやつ。というか正式なフォーマットがどこかにあるんだろうな。人に教えるほど何かを極めているわけでもないのにどこへ向かっているんだろうか私は。

スマホで見るとビールのラベルっぽい。

VSCode で Ruby 書くための設定

PC 変えた時に vim & ruby のいい感じの設定が失われてしまいしばらく書かないから放置していたらまた書くことになり VSCode でなんとなく書いてたらいい加減不便さを感じてきたので本当にとりあえずの設定を調べました。(早口)

この辺ができるように設定。

  • 構文チェック
  • 補完と定義ジャンプ

あとこれは

でやってます。

やること

VSCodeRuby の拡張があるので入れる (名前そのまんま Ruby のやつ)

GitHub - rubyide/vscode-ruby: Provides Ruby language and debugging support for Visual Studio Code

拡張を動かすために rubocop があればいいそうなので入れる

gem install rubocop

VSCode の設定ファイルに以下を追加

    "ruby.lint": { "rubocop": true },
    "ruby.useLanguageServer": true,
    "ruby.intellisense": "rubyLocate",

説明

  1. "ruby.lint": { "rubocop": true }: Linter は rubocop
  2. "ruby.useLanguageServer": true: Language Server 使う (構文チェックしてくれるようになる)
  3. "ruby.intellisense": "rubyLocate": インテリセンスの設定 (ある程度、補完、定義ジャンプができるようになる)

終わり

rubyLocate のオプションには定義検索するパスが設定できるようで、うまくやれば結構便利になるかもです?脳死で初期設定できないと割と放置しがちな癖があり、よく無いですね。

open-uri の close とか

rubyopen-uriopen で、サイトをスクレイピングするプログラムを見かけたのだけれど、「close 呼ばなくていいの?」と思って色々調べたのでメモしておきます。結論としては、ブロック渡した時は呼ばなくていいけど基本的には呼ぶ。

調べたのは ruby 2.3 なので古くなるかもです、がそんなに変わらなそうな部分?

ブロック渡した時は呼ばなくていい

open メソッドの実装を見てみます。

> require 'open-uri'
=> true
> method(:open).source_location
=> ["/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/open-uri.rb", 29]

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L29-L39

  def open(name, *rest, &block) # :doc:
    if name.respond_to?(:open)
      name.open(*rest, &block)
    elsif name.respond_to?(:to_str) &&
          %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://} =~ name &&
          (uri = URI.parse(name)).respond_to?(:open)
      uri.open(*rest, &block)
    else
      open_uri_original_open(name, *rest, &block)
    end
  end

今回は引数として url 文字列の見渡すので、引数 name は URL として解釈され、URI#open が呼ばれるみたいです。そこだけ切り出して実行してみます。

> (uri = URI.parse('https://ushumpei.hatenablog.com')).respond_to?(:open)
=> true
> uri
=> #<URI::HTTPS https://ushumpei.hatenablog.com>
> uri.method(:open).source_location
=> ["/System/Library/Frameworks/Ruby.framework/Versions/2.3/usr/lib/ruby/2.3.0/open-uri.rb", 716]

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L716-L718

OpenURI::OpenRead#open が呼ばれるみたい。その中から OpenURI.open_uri が呼ばれている。

https://github.com/ruby/ruby/blob/ruby_2_3/lib/open-uri.rb#L132-L166 (抜粋)

    if block_given?
      begin
        yield io
      ensure
        if io.respond_to? :close!
          io.close! # Tempfile
        else
          io.close if !io.closed?
        end
      end
    else
      io
    end

153 行目で block_given? によりブロックが与えられていた時は close を呼ぶように実装されている (ローンパターンか)

なのでブロック渡せば close は明示的に呼ばなくてもいいことがわかりました。

気になったコード

html = open('https://ushumpei.hatenablog.com').read

え、これ絶対 close されないのでは?と思ったので lsof で開きっぱなしなことを確認しました。確認コマンドとその意味は以下の感じになります。

$ lsof -c ruby | grep open-uri | wc -l
  • ruby が開いているファイルを見たいので -c ruby でコマンド名を指定しました。
  • 一時ファイル名に open-uri が付いていたのでそれで絞り込み
  • 行数取得

該当コード実行前の lsof -c ruby | grep open-uri | wc -l0

該当コード実行 (irb は開きっぱなし)

> require 'open-uri'
=> true
> 10.times { |n| open('https://ushumpei.hatenablog.com').read }
=> 10

該当コード実行後の lsof -c ruby | grep open-uri | wc -l10irb を終了するとファイル数は 0 に戻りました。

ブロック渡した時は irb 開きっぱなしの状態でもファイル数は 0 のままです。

(注意: open-uriopen した時に、lsofopen-uri がひっついていている行が出力される、という事象は、ソースコードレベルでは確認しきれていません。ちょっとお粗末になり申し訳ないですが、「多分 open 時に作成された開かれているファイルの行だ」という予想に基づいている、と断っておかなければいけません)

まとめ

  • open(XXX).readopen(XXX) { |f| f.read } とかに変えよう
  • lsof って list open files なんですね。ポート使っているプロセス調べるコマンドかと思っていた。
  • というか linux が全てをファイルという形で抽象化しているアレのアレなのか。(よく知らない)
  • そういえば open って |ls とかでパイプつけたコマンド渡すと実行できたりしてアレらしいので、OpenURI.open_uri 呼んじゃった方がいいっぽい。

追記 2019/04/07

  • 可読性微妙かもだけど、 open(XXX).read の書き換え open(XXX, &:read) もいけるのか、 ruby すごい

静的 html を BASIC 認証付きで雑に公開する方法

雑なメモです (なんかやばかったら教えていただきたいです)

1. 適当なリポジトリを作成する

$ mkdir private
$ cd private
$ git init

2. html ファイルとかを配置する

$ echo '<!DOCTYPE html><html><head><title>private</title></head><body><h1>I am private!</h1></body></html>' > index.html

3. heroku アプリを作成して、 php のビルドパックを追加する。php プロジェクトだと認識させるために composer.json を作成する。

これ、 php じゃないのに php 使う気持ち悪さがあります、多分違う方法ありそう。

$ heroku create
$ heroku buildpacks:set heroku/php
$ composer init

4. .htaccess.htpasswd を作成する

デフォルトの設定で apache を使用するようになっているけど、Procfileweb: heroku-php-apache2 とか書くほうが確実かも

$ echo 'AuthUserFile /app/.htpasswd
AuthType Basic
AuthName "Restricted Access"
Require valid-user' > .htaccess
$ htpasswd -c ./.htpasswd なんか好きなユーザー名

(名前が .ht* のファイルにはアクセス制限かけてるみたいだけど、リポジトリ.htpasswd 含めるのってどうなのだろう)

5. 必要なものステージしてコミットして heroku に push する。

$ git add .
$ git commit -m 'Initial commit.'
$ git push heroku master

6. 終わり

$ heroku open

PostgreSQL 10 でパーティションをまるっと切り替えてみる

PostgreSQL の 10 では宣言的パーティショニングが使えるようになったそうですね (しかし僕は 10 以前でパーティショニングしたことがなかったので感想がない)

これを使ってデータの一括削除、一括追加を実現するために、パーティションの切り替えについて練習します。

準備

Docker で PostgreSQL 10 を用意します

$ docker run --name postgres -p 5432:5432 -d postgres:10

テーブルの作成

こんな感じのテーブルを作ります。ユーザーがいつどこに居たかをひたすら集める謎のテーブルです。

user_id latitude longitude created_at
integer decimal(11, 8) decimal(11, 8) timestamp

パーティションcreated_atrange で切って見ます。

create database testgres;

\c testgres

create table locations (
  user_id integer not null,
  latitude decimal(11, 8) not null,
  longitude decimal(11, 8) not null,
  created_at timestamp not null
) partition by range (created_at);

パーティションの作成

パーティションは partitions スキーマ以下に作っていきます。一秒刻みで適当なデータを作成します。 20181010 と 20181011 のパーティションを作成しました。

create schema partitions;

create table partitions.locations_20181011 partition of locations for values from ('2018-10-11') to ('2018-10-12');

insert into partitions.locations_20181011 select
  (random() * 10000)::int,
  130 + (20 * random())::decimal(11, 9),
  20 + (20 * random())::decimal(11, 9),
  generate_series('2018-10-12'::timestamp - '1 sec'::interval, '2018-10-11', -'1 sec'::interval)
;

create table partitions.locations_20181010 partition of locations for values from ('2018-10-10') to ('2018-10-11');

insert into partitions.locations_20181010 select
  (random() * 10000)::int,
  130 + (20 * random())::decimal(11, 9),
  20 + (20 * random())::decimal(11, 9),
  generate_series('2018-10-11'::timestamp - '1 sec'::interval, '2018-10-10', -'1 sec'::interval)
;

クエリしてみると、ちゃんとパーティションを使った検索が行われているのがわかります。

explain select * from locations where created_at = '20181010 12:00:00';

                                    QUERY PLAN
-----------------------------------------------------------------------------------
  Append  (cost=0.00..1716.00 rows=1 width=28)
    ->  Seq Scan on locations_20181010  (cost=0.00..1716.00 rows=1 width=28)
          Filter: (created_at = '2018-10-10 12:00:00'::timestamp without time zone)
(3 rows)

パーティションの切り替え

f:id:ushumpei:20181015011230j:plain

以下が概要になります。

  1. 普通のテーブルとして locations_20181011 を作成します。
  2. 現在のパーティション partitions.locations_20181011locations から Detach します
  3. 新しいパーティションとして locations_20181011locations に Attach します
  4. 必要なくなった partitions.locations_20181011 を削除します
  5. (跡片付け) 新しいパーティションとして Attach した locations_20181011partitions.locations_20181011 にリネームします

手順 3, 4 と、なるべく alter table locations をまとめて行うことで、テーブルロック時間を少なくしていこうと思います。

新しく locations_20181010 を普通のテーブルとして public スキーマに作成します。(すでにあるものと被らなければ他の方法でもいい)

ここでは check 制約を追加して、Attach される際のパーティションの制約チェックを先立って行っています。これがないとテーブルロック時間が attach の際に増えてしまうそうです。

create table locations_20181011 (
  user_id integer not null,
  latitude decimal(11, 8) not null,
  longitude decimal(11, 8) not null,
  created_at timestamp not null,
  check (created_at >= '2018-10-11' and created_at < '2018-10-12')
);
insert into locations_20181011 select
  (random() * 10000)::int,
  130 + (20 * random())::decimal(11, 9),
  20 + (20 * random())::decimal(11, 9),
  generate_series('2018-10-12'::timestamp - '1 sec'::interval, '2018-10-11', -'1 sec'::interval)
;

パーティションの切り替えを行います。現在使用されている partitions.locations_20181011 を切り離し、新しく作った locations_20181011 をくっつけます。

begin;
alter table locations detach partition partitions.locations_20181011;
alter table locations attach partition locations_20181011 for values from ('2018-10-11') to ('2018-10-12');
commit;

いらなくなったパーティションの削除、跡片付けとして作成したパーティションのリネームを行います。

drop table partitions.locations_20181011;
alter table locations_20181011 set schema partitions;

以上です。

感想

  • テーブルロック怖い
  • トリガー怖い
  • インデックスも適切に