まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:nodejs

最近 OCP(Openshift Container Platform) について調べることが多かったので、何回かに分けてアウトプットしていこうと思います。

今回は「OCP のアップグレードパス」についてです。OCP のアップグレード(バージョンアップ)で特に頭の痛い問題の1つが「アップグレードパス」と呼ばれるバージョンアップ時に踏む段階のことです。

より具体的な例で考えてみましょう。例えば、現在 OCP 4.4.6 というバージョンを使っていると仮定します。なんらかの理由でこれを OCP 4.6.4 というバージョンにアップグレードしたい、という前提のもとで以下を記載していきます。


【アップグレードパス】
この辺りの事情にあまり詳しくない人だと「OCP 4.4.6 から OCP 4.6.4 へのアップグレードがそんなに不便なのか?」という疑問を持つかもしれません。わかりにくい所もあるのですが、最大の問題は「そもそも OCP 4.4.6 から OCP 4.6.4 へ直接アップグレードできるのか? 直接アップデートできない場合はどのような順で実行していけばアップデートできるのか?」という所から解決していく必要がある点にあります。この「どのような順で実行していく」のかという順序のことを「アップグレードパス」といいます。

例えばですが、仮に 4.4.6 から 4.6.4 への直接アップグレードが可能であった場合(実際はできないんですけど)、アップグレードパスは「 4.4.6 → 4.6.4 」ということになります。一方、もしも間に 4.5.0 をはさんで 4.4.6 から 4.5.0 にアップグレードし、4.5.0 から 4.6.4 にアップグレードするという2段階のアップグレードを行う必要がある場合(実際は更に複雑なんですが)、アップグレードパスは「 4.4.6 → 4.5.0 → 4.6.4 」ということになる、というわけです。


【アップグレードパスの調べ方】
さて、では実際に 4.4.6 から 4.6.4 へアップグレードする場合のアップグレードパスはどのように調べればよいのでしょうか? 実はこれが結構面倒だったりします。。

まず RedHat 提供のアップグレードパスを探すサービスがあります:
https://access.redhat.com/labs/ocpupgradegraph/update_path

ただこのサイト、比較的古いバージョンが対象だとやけに重くなってしまうのです。またアップグレード前後のバージョン差が大きいケースだと「アップグレードパスが見つからない」みたいな結果が表示されることもあり、なんかちょっとよくわからない(苦笑)感じだったりします。


というわけで、上記サービスに頼らないアップグレードパスの探し方を紹介します。具体的にはまずゴールとなるバージョン(今回だと 4.6.4)のリリースノートを調べる必要があります。OCP のバージョンごとのリリースノートはオンラインで参照することができ、その URL は以下の通りです:
https://mirror.openshift.com/pub/openshift-v4/clients/ocp/(バージョン)/release.txt

例えばバージョンが 4.6.4 であれば、以下の URL を参照します:
https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.6.4/release.txt

以下のような内容が表示されます:
2024030701


はじめの十数行だけを表示するとこのようになっています:
Client tools for OpenShift
--------------------------

These archives contain the client tooling for [OpenShift](https://docs.openshift.com).

To verify the contents of this directory, use the 'gpg' and 'shasum' tools to
ensure the archives you have downloaded match those published from this location.

The openshift-install binary has been preconfigured to install the following release:

---

Name:      4.6.4
Digest:    sha256:6681fc3f83dda0856b43cecd25f2d226c3f90e8a42c7144dbc499f6ee0a086fc
Created:   2020-11-11T15:13:14Z
OS/Arch:   linux/amd64
Manifests: 444

Pull From: quay.io/openshift-release-dev/ocp-release@sha256:6681fc3f83dda0856b43cecd25f2d226c3f90e8a42c7144dbc499f6ee0a086fc

Release Metadata:
  Version:  4.6.4
  Upgrades: 4.5.16, 4.5.17, 4.5.18, 4.5.19, 4.6.1, 4.6.2, 4.6.3
  Metadata:
    description: 
  Metadata:
    url: https://access.redhat.com/errata/RHBA-2020:4987
  :
  :

↑の赤字部分に着目します。"Release Metadata:" と書かれた部分の下にバージョンに関する情報が表示されています。まず "Version:" はこのリリースノートが対象としているバージョン(4.6.4)が表示されています。 そしてその下の行の "Upgrades:" に着目してください。この例だと "4.5.16, 4.5.17, 4.5.18, 4.5.19, 4.6.1, 4.6.2, 4.6.3" と書かれていますね。

これはつまり「バージョン 4.6.4 に直接アップグレードできるバージョンは 4.5.16, 4.5.17, 4.5.18, 4.5.19, 4.6.1, 4.6.2, 4.6.3 のいずれかのみ」であることを示しています。残念ながら元のバージョンである 4.4.6 が含まれていないので、少なくとも1回のアップグレードで 4.4.6 から 4.6.4 へはアップグレードできないことも分かります。

では最終ゴールである 4.6.4 へは(上のバージョンの中のどれでもいいんですが、なるべく回数を減らしたいので最も遠い) 4.5.16 からアップグレードするとしましょう。次に調べる必要があるのは「4.4.6 から 4.5.16 へ直接アップグレードできるのか?」です。

これも同様にして 4.5.16 のリリースノート(https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.5.16/release.txt)を開き、4.5.16 にアップグレード可能なバージョンを調べると、"4.4.13, 4.4.14, 4.4.15, 4.4.16, 4.4.17, 4.4.18, 4.4.19, 4.4.20, 4.4.21, 4.4.23, 4.4.26, 4.4.27, 4.4.28, 4.4.29, 4.4.30, 4.5.2, 4.5.3, 4.5.4, 4.5.5, 4.5.6, 4.5.7, 4.5.8, 4.5.9, 4.5.11, 4.5.13, 4.5.14, 4.5.15" という結果になることが分かります。残念ながら 4.4.6 は含まれていないので、更に段階を経たアップグレードが必要になることが分かります:
2024030702


同様にして、ここでの中継バージョンを 4.4.13 とみなし、4.4.13 のリリースノート(https://mirror.openshift.com/pub/openshift-v4/clients/ocp/4.4.13/release.txt)を調べると、、今度は "4.4.6" が含まれていることがわかります。4.4.6 から 4.4.13 へのアップグレードは可能でした:
2024030703


ここまでの結果を総合すると、4.4.6 から 4.6.4 へのアップグレードパスの1つとして
4.4.5 → 4.4.13 → 4.5.16 → 4.6.4

があることが確認できました。つまり 4.4.6 から 4.6.4 へは少なくとも3回に分けてアップグレードを実施する必要がある、ということになります。

今回の例では 4.4.6 から 4.6.4 という固定バージョンでのアップグレードパスと、その調べ方を紹介しましたが、アップグレードパスの調べ方はどのバージョンからどのバージョンへ向かう場合も同様です。上で紹介した方法を使って、目的バージョンから「このバージョンにアップグレードできる最も古いバージョンは?」を繰り返し調べていくことで、最短回数でのアップグレードパスを見つけることができるようになります。

・・・でも、ちょっと面倒ですよね。そこでこの処理をツール化してみました。


【アップグレードパスを調べるツール】
上で紹介した処理を自動的に調べるツールを Node.js で作ってみました。MIT ライセンスで公開しているので商用含めて利用・改変もご自由に:
https://github.com/dotnsf/ocp-upgrade-path


Node.js がインストールされた PC 上で、上記 URL からソースコードを clone またはダウンロードして展開します。最初の実行前に一回だけ "npm install" で外部ライブラリをインストールしておく必要があります。

実行時はコマンドラインから以下のように指定して実行します:
$ node app (現在のバージョン) (アップグレード後のバージョン)

上のように現在のバージョンが 4.4.6 、アップグレード後のバージョンが 4.6.4 であったとすると、以下のように指定して実行することになります:
$ node app 4.4.6 4.6.4

実行結果は以下のように出力されます:
$ node app 4.4.6 4.6.4
currentversion = 4.4.6
targetversion = 4.6.4

4.4.6 -> 4.4.13 -> 4.5.16 -> 4.6.4

不可能なパターンを指定するとアップグレードパスに impossible と表示されます(新しいバージョンから古いバージョンへのアップグレードを指定した例です):
$ node app 4.6.4 4.4.6
currentversion = 4.6.4
targetversion = 4.4.6

4.6.4 -> (impossible) -> 4.4.6

またバージョンで指定できるパターンは正式リリース対象の (数字).(数字).(数字) だけです。バージョン文字列内に "nightly" などの文字を含む正式リリースではないバージョンも存在していますが、このツールでは対象外としています。

ちゃんとテストしたわけではない(というかテストできない)のですが、現時点でリリースされていない未来のバージョンについても、その未来バージョンがリリースされて、リリースノートも同様に提供されていれば、その未来バージョンへのアップグレードパスも表示できるようになるはずです。

ウェブ化してもっと気軽に(Node.js がインストールされていなくても)使えるようにすることも考えたのですが、「OCP をバージョンアップする」作業が必要になるようなプロジェクトでは、それなりのエンジニアがプロジェクトに携わっていることが大半だと思ってサボっちゃいました。


「OCP アップグレード版の乗換案内」的なツールができたと思っています。役立つことがあれば嬉しいです。


【参考】
Successfully upgrade OpenShift cluster on a disconnected environment with troubleshooting guide.



ツイッター(X)が API 制約を含めて色々と不便になってしばらく経過しました。ツイッターでリツイートボットとか作ってた個人のものはほぼ全滅状態ですが、さすがに「しばらく待てばまた使えるようになる、、」などと楽観的には考えない方が良さそうな感じですね。

世の中には使いにくくなったツイッターの後釜を狙う SNS も多く出てきましたが、今のところはまだツイッターを使っている人が圧倒的多数の印象です。まあ開発者的にはツイッターはもう見限ってる人も少なくないんでしょうけど、SNS はあくまで「発信の場」であって、使っているのは開発者ばかりじゃないですからね。メッセージを届ける先の規模含めると、使い勝手が現状維持ならまだしばらくツイッターが使われていくのでしょう。


一方で、とはいえ多くの SNS が新たに生まれてきてはいます。実は自分も「お絵描き SNS」という位置づけの MyDoodles なんていうものを密かにリリースしていたりもするのですが、、、そんな中でツイッターの共同創業者の一人だったジャック・ドーシーさんが新たに立ち上げた分散型 SNS である Bluesky Social (以下 "Bluesky")はツイッターに似たインターフェースを持ち、リリースからしばらくは招待制で運用されていましたが、先ごろ招待制が撤廃されて誰でもアカウントを作れるようになりました。比較的早い段階から API も公開されており、開発者視点では「次に来るのはこれかも」と期待しています。


そんな Bluesky の API を使って、ツイッターでは(個人では事実上)作れなくなってしまったボットを作ってみることにしました。自分が作ろうと思ったのは拙作マンホールマップが持つ機能の1つである「今日のマンホール」を午前零時に自動でつぶやくボットです。実はこれまでは私自身が人間ボットになって(笑)今日のマンホールを手動でつぶやいていたのですが、これを Bluesky 内で自動化する、というものです。ボットとしての基本的な考え方はツイッターの頃と大きく変わるものではないのですが、分散 SNS である Bluesky の、まだ充分に熟しているとは言えない API を使って実装することはチャレンジでもあり、色々面倒な考慮も必要にはなるのですが、とりあえず作れそうだったので、その中身の解説をするのが本ブログエントリの目的です。なお以下で紹介する内容は 2024-02-12 時点の情報であることをご了承ください。


【開発環境】
まず Bluesky API を紹介している本家ページがこちらです:
https://www.docs.bsky.app/docs/get-started

2024021201


SDK としては Node.js と Python 向けに提供されているようですが、Curl からも使える REST API があるようなので REST API でプログラミングするつもりであれば事実上プログラミング言語に制約はないことになります。今回以下で紹介するプログラミングの環境としては自分が普段使っている Node.js を使うことにします。

Node.js の場合、@atproto/api というライブラリを使うことで比較的簡単に Bluesky API を使ったアプリケーションの開発ができるようになります。こんな感じでインストールして使います:
$ npm install @atproto/api


【認証】
Bluesky API を使って試しに自分自身の情報でも取得・・・ するのもいいですが、まずは認証を行う必要があります。ツイッターなどでは開発者向けページからアプリケーションを登録して API キーを発行して OAuth 2.0 で・・・ といった手順で認証を行っていました。Bluesky もいずれはそのような形態になるものと思いますが、現時点では OAuth2.0 はサポートされておらず、ユーザー ID とパスワードを使って直接ログインする形になるようです。ちょうど Bluesky の招待制も終わったことなので、自分はこのボット用(というかマンホールマップ用)のアカウントを1つ新たに作成しました。そして Node.js の場合はこのような形で認証(ログイン)を行います:
var { BskyAgent } = require( '@atproto/api' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  // ログイン成功後の処理へ
    :
    :
});

"@atoproto/api" ライブラリから BskyAgent をインポートして、メールアドレス(上では "myname@email.com" の部分)とパスワード(同 "mypassword" 部分)を指定してログインします。このあたりはソースコード内に直接記載するのは危険なので、コードの管理方法には注意が必要です。


【ツイートする】
ボットを作るには「ツイートする」(書き込む)機能が必要です。ログイン後に単にテキストをツイートするだけなら簡単です。特に @atproto/api ライブラリを使っていれば post メソッドを使って以下のようにログイン後の処理を一行追加するだけです:
var { BskyAgent } = require( '@atproto/api' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  var res = await agent.post( { text: 'Hello, world.'} );
  console.log( {res} );
});

これでログインしたユーザーの権限で "Hello, world." とつぶやくことができます。簡単ですよね、ここまでは。


【facets 処理(リッチテキスト処理)】
テキストをつぶやくのは簡単でした。ツイッターなどではこのテキスト内に他のユーザーへのメンション(@)が含まれていたり、ハッシュタグ(#)やリンクが含まれていると自動的に解釈してリッチテキスト化してくれて(リンクとかを付けてくれて)いましたが、Bluesky API はそうはいきません(そもそも Bluesky にはまだハッシュタグという概念がありません)。この辺りはプログラミング内でリッチテキスト化(Bluesky では「facets 検知」といいます)する必要があります。今回作ろうとしているボットも必ずあるページへのリンクを含む内容になっているので post 実行前の facets 検知が必須です。この辺から少し面倒になってきます。

具体的にはこのようなコードになります:
var { BskyAgent, RichText } = require( '@atproto/api' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  var text = "リンクを含むテキスト https://manholemap.juge.me/";

  var rt = new RichText({ text: text });
  await rt.detectFacets( agent );

  var res = await agent.post({
    $type: 'app.bsky.feed.post',
    text: rt.text,
    facets: rt.facets
  });

  console.log( {res} );
});

まずリッチテキスト化処理を行うために @atproto/api ライブラリから BskyAgent だけでなく RichText もインポートしておきます。そしてリンクなどのリッチテキスト化が必要なテキストを RichText で初期化した結果を変数 rt で受け取り、更に detectFacets() 処理を実行します。するとテキスト部分は rt.text に、facets 情報が rt.facets にそれぞれ格納されます。ツイッターではこの辺りの処理が自動化されていたので楽でしたが、Bluesky でもここまでライブラリが用意されているのでさほど大変ではないですね。

最後に facets 処理した結果を post します。先のプレーンテキストでは text 要素だけを post していましたが、リッチテキストの場合は少し情報を加えてあげる必要がありますが、これでテキスト内の URL 部分はリンクになってポストされます。


【オープングラフ処理(OGP対応)】
ツイッターなど多くの SNS ではリンクを含む情報をポストすると、そのリンク先の内容の一部(テキストや画像)が埋め込まれる形で表示されます。これは OGP と呼ばれる規格で、リンク先が OCP 規格に沿って作られたページであれば、その情報を使って実現できるものです。

Bluesky の場合もリンク先の情報を埋め込むことはできるのですが、API でつぶやく場合は(現状では)この部分を全て手作業で行う必要があります。具体的にはこんな感じです:
var { BskyAgent, RichText } = require( '@atproto/api' );
var { parse } = require( 'node-html-parser' );

var agent = new BskyAgent({
  service: "https://bsky.social"
});
agent.login({
  identifier: "myname@email.com",
  password: "mypassword",
}).then( async function(){
  var text = "リンクを含むテキスト https://manholemap.juge.me/";

  //. OGP
  var url = "https://manholemap.juge.me/";
  var resp = await fetch( url );
  var html = parse( await( resp.text() ) );
  var title = html.querySelector( "meta[property='og:description']" ).getAttribute( "content" );

  //. embed image
  var ogpImg = html.querySelector( "meta[property='og:image']" ).getAttribute( "content" );
  var blob = await fetch( ogpImg );
  var buffer = await blob.arrayBuffer();
  var response = await agent.uploadBlob( new Uint8Array( buffer ), { encoding: "image/jpeg" } );
  var embed_params = {
    $type: "app.bsky.embed.external",
    external: {
      uri: "https://manholemap.juge.me/",
      thumb: {
        $type: "blob",
        ref: {
          $link: response.data.blob.ref.toString()
        },
        mimeType: response.data.blob.mimeType,
        size: response.data.blob.size
      },
      title: title,
      description: title
    }
  };

  var rt = new RichText({ text: text });
  await rt.detectFacets( agent );

  var res = await agent.post({
    $type: 'app.bsky.feed.post',
    text: rt.text,
    facets: rt.facets,
    embed: embed_params
  });

  console.log( {res} );
});

まず URL のリンク先情報を解析するために node-html-parser ライブラリをインポートしておきます。そしてリンク先の URL(上では "https://manholemap.juge.me/")を fetch して HTML を取得します。OGP の規格に沿って作られたページであれば、その画像は HTML 内で <meta property="og:image" content="***" /> の *** 部分に指定されることになっているのでその値を取り出します。そして再度その画像 URL を fetch してバイナリデータを取り出し、agent 変数を使ってアップロードします。そのアップロード結果の情報等を使って OGP の埋め込みデータ(embed_params 変数)を作成します。埋め込みデータを作ることができたら、その値を embed パラメータに加えて post する、という流れになります。これで OGP に対応したポスト処理を Node.js で行うことができるようになりました。


【サンプルソースコード公開】
後はこの実行を自動化することになりますが、今回は自分が使っているサーバーの crontab を使って日本時間の午前零時ちょうどに実行するようにしました。具体的なコードは後述しますが、大まかには、
(1)日本時間午前零時になったら、マンホールマップの「今日のマンホール」取得 API を呼び出し、
(2)その結果からリンク先 URL やポストするテキストを生成し、
(3)リンク先 URL の OGP 情報を使って画像やタイトルなどを取り出して、埋め込み、
(4)facets 処理を加えた上でポストする

という流れを行っています。そしてそのサンプルソースコードを以下で公開しています:
https://github.com/dotnsf/bsky_manhotalk_bot/blob/main/motd.js


上では解説していない部分を1点だけ。上述のように現状の Bluesky API は認証情報を ID とパスワードを直接指定する形で実行する必要があります。上のリンク先を見るとわかるのですが、今回公開したサンプルソースコードには ID やパスワードは含まれておらず、このソースコードを実行する際の環境変数から取得するようにしています。したがってこのソースコードを実行する時に、
$ BSKY_ID=myname@email.com BSKY_PW=mypassword node motd.js

といった具合に BSKY_ID 環境変数と BSKY_PW 環境変数にそれぞれ Bluesky へのログイン ID とパスワードを指定して実行することで正しく動くようにしています。

そうして作ったマンホールマップ・ボットはこちらのアカウントで公開しています:
https://bsky.app/profile/manholemap.bsky.social

2024021200


↑ツイッターとちがって Bluesky のアカウントを持っていない人でも見ることはできますが、可能であればアカウントを作ってフォローしてください。「今日のマンホール」が登録されている日全てで午前零時になると教えてくれます(登録されていない日もあります)。

とりあえず現時点では「今日のマンホール」専用で動くボットですが、いずれハッシュタグが実装されたら #manhotalk ハッシュタグを検索して・・といった挙動や、ちょっとした会話にも挑戦するつもりでいます。

自作サービスを開発している途中で調査した内容のアウトプットです。

Node.js を使って PostgreSQL データベースを操作する場合、pg(node-postgres) ライブラリを使うのが定番だと思っています。実際これまで何度も使ってきているし、データの読み書き更新削除といった作業で特に困ったことはありませんでした。

しかし今回ちょっとしたことで詰まってしまいました。結果的には解決できたのですが、データベース内に定義されたテーブルの、列の定義情報を調べたいと思った時に「これどうやるんだろ?」となってしまいました。

もう少し具体的に説明します。例えば以下のような SQL を使って items テーブルを作成したとします:
CREATE TABLE items( id varchar(50) primary key, name varchar(100) not null, price int default 0, body text, image bytea, datetime timestamp );

このようにして作成した items テーブルの各列の定義情報(上の例の青字部分)を取り出す方法が分からなかったのでした(列名だけであれば select 文の実行結果の中に含まれるので、1行でもレコードが登録されていればそこから分かる、ということは知っていました。が、レコードが1件も登録されていないケースだったり、列名以外の型の情報まで必要な場合の取得方法が分かっていませんでした)。ちなみにこの情報は psql コマンドを使った場合はログイン後に
# \d items

というコマンドを実行することで取得できることは知っていました("items" の部分に知りたいテーブル名を指定して実行します):
db=# \d items
                          Table "public.items"
  Column  |            Type             | Collation | Nullable | Default
----------+-----------------------------+-----------+----------+---------
 id       | character varying(50)       |           | not null |
 name     | character varying(100)      |           | not null |
 price    | integer                     |           |          | 0
 body     | text                        |           |          |
 image    | bytea                       |           |          |
 datetime | timestamp without time zone |           |          |
Indexes:
    "items_pkey" PRIMARY KEY, btree (id)

この方法を知っていたので、これまであまり気にすることもありませんでした。ところがこれはあくまで psql コマンドを利用する際のコマンドであって、これをそのまま SQL として pg を使って実行すると(SQL ではないので当然ですが)エラーとなってしまいます。ではいったいどうすれば pg でこの情報をプログラムのコード内で取り出すことができるのだろうか・・・ というのが今回のブログエントリのテーマです。


結論として分かったのは、こんな感じでした:
・SQL としては実行結果にすべての列が含まれるような SELECT 文(例: "select * from items")を実行する
・実行結果からレコードを取り出す場合は result.rows を参照するが、実行結果の列情報は result.fields と result._types._types.builtins を参照することで取り出すことができる
・実行結果のレコードが0件でも(1件もレコードが登録されていなくても)、上の方法で列情報を取り出すことはできる

具体的なコードとしてはこのような感じです:
var PG = require( 'pg' );
var pg = new PG.Pool({
  connectionString: "postgres://user:pass@hostname:5432/db",
  idleTimeoutMillis: ( 3 * 86400 * 1000 )
});

  :
  :

if( pg ){
  var conn = await pg.connect();
  if( conn ){
    try{
      var sql = 'select * from items';
      var query = { text: sql, values: [] };
      conn.query( query, function( err, result ){
        if( err ){
          console.log( err );
        }else{
          var fields = r1.result.fields;
          var types = r1.result._types._types.builtins;
          var columns = [];
          fields.forEach( function( f ){
            var dt = Object.keys( types ).reduce( function( r, key ){
              return types[key] === f.dataTypeID ? key : r;
            }, null );
            columns.push( { column_name: f.name, type: dt } );
          });

            :
            :
        }
      });
    }catch( e ){
      console.log( e );
    }finally{
      if( conn ){
        conn.release();
      }
    }
  }
}


赤字の部分の解説をします。まず "postgres://(ユーザー名):(パスワード)@(PostgreSQL サーバー名):(ポート番号)/(DB名)" というフォーマットの接続文字列を使ってデータベースに接続します(正しく接続できるのであれば、このフォーマットである必要はありません)。 そして接続後に "select * from items" というシンプルな SQL を実行して、結果を result という変数で受け取ります。この SQL 実行結果(レコード情報)自体は result.rows という属性に配列形式で格納されているのですが、今回ここは使いません。

この SQL を実行することにより、指定したテーブル(今回の場合は items)の列名とデータ型IDの情報が result.fields に、データ型IDとデータ型の関係を示す表が result._types._types.builtins に格納されているはずです。これらを取り出し、各列のデータ型を ID ではなく文字列に変換しなおして、最終的に columns という配列変数に記録しています。

この columns の実行結果を参照すると、このような値になっているはずです:
    [
      {
        "column_name": "id",
        "type": "VARCHAR"
      },
      {
        "column_name": "name",
        "type": "VARCHAR"
      },
      {
        "column_name": "price",
        "type": "INT4"
      },
      {
        "column_name": "body",
        "type": "TEXT"
      },
      {
        "column_name": "datetime",
        "type": "TIMESTAMP"
      }
    ]

"integer" 型が、より正確な "INT4" という型になっていたりはしますが、当初取得したかった列の定義情報を取得することができました。なお、この方法であれば SQL の実行結果(result.rows)そのものを参照しているわけではないため、実行結果が0件であっても(レコードがまだ1件も登録されていない場合でも)実行できるようです。

サンプルソースコードはこちらからどうぞ:
https://github.com/dotnsf/pg_fieldtype


(2023-12-18 追記)
ちなみに MySQL の場合、その名もズバリの mysql ライブラリを使うのが定番だと思ってますが、こちらの場合はテーブル一覧("show tables")もテーブル定義("desc (テーブル名)")も、CLI で使う命令文をそのまま利用して取得することができるので、深く考えなくてもよいのでした。




ChatGPT で話題になっている OpenAI からは API や SDK が提供されており、これらを使うことで、自作アプリケーションに ChatGPT のような機能を組み込むことができるようになります。API 自体は無料登録でも(バージョン 3.5 以下を)利用することができますが、有料版にすることで(精度が話題になっている)バージョン 4.0 の API を使うこともできるようになる、というものです。

・・・というものなのですが、実際に ChatGTP と同様に問い合わせをする API を実行してみるとかなりの確率で 429 という HTTP ステータスコードのエラーが返されます。この 429 は "Too many requests" を意味するエラーコードです。特に無料登録の場合は利用回数に制限があったり、API 実行時のリソース割り当て優先度が低く設定されてしまい、「(他に使われていて)混み合っている時は実行できない」ということが頻繁に発生するのでした。

この問題に関しては OpenAI の記事でも取り上げられていて、「(API 実行時に)"exponential backoff" で実行するようにしてほしい」と紹介されています:
https://help.openai.com/en/articles/5955604-how-can-i-solve-429-too-many-requests-errors


↑の記事では(exponential backoff 関数が標準で使える) Python での簡単なサンプルが紹介されています。個人的には JavaScript で実装することが多いので JavaScript のサンプルを探してみました。OpenAI API ではない例で exponential backoff を JavaScript で実装している例は見つけることができた(↓)のですが、OpenAI API のサンプルは軽く探した限りでは見つかりませんでした:
https://advancedweb.hu/how-to-implement-an-exponential-backoff-retry-strategy-in-javascript/


・・・というわけで自分で作ってみました、というブログエントリです。Node.js での実装サンプルはこちらの GitHub で公開しています:
https://github.com/dotnsf/openai-api


【そもそも exponential backoff とは?】
"exponential" は「指数的」という意味で、「指数的に増加する段階ウェイトをかけながら(何かを)繰り返し実行する」ことです。
2023060800


今回の例だと(ChatGPT の)API を実行して 429 エラーになった場合、1秒待って再実行し、また 429 エラーになったら1秒待って再実行し、・・・ という方法ではありません。これだとウェイトは増加することなく、常に1秒待って再実行を繰り返しているだけです。

「指数的」という名前の通り、ウェイトに指数を使います。例えば「2の〇乗」という指数を使った場合で計算すると、まず API を実行して 429 エラーになった場合、まず(2の0乗=)1秒待って再実行し、また 429 エラーになった場合、次は(2の1乗=)2秒待って再実行し、また 429 エラーになった場合は次は(2の2乗=)4秒待って再実行し、また 429 エラーになったら(2の3乗=)8秒待って再実行し、・・・ といった具合にウェイト時間が指数関数で増えていくようなループ処理を行うことになります(上の GitHub 公開サンプルの場合、秒ではなくミリ秒の単位で計算しています)。これが exponential backoff です。

特定条件下で 429 エラーが高い確率で返される可能性が高いとわかっているような API を使う時の、ベストプラクティス的な実装方法のようです。


【実装例】
Node.js(JavaScript)での実装例を紹介します。例えば exponential backoff 実装前はこのような形で実行していました:
var { Configuration, OpenAIApi } = require( 'openai' );
var configuration = new Configuration({ apiKey: "(API KEY)", organization: "" });
var openai = new OpenAIApi( configuration );
  :
  :

var option = {
    model: "gpt-3.5-turbo",
    prompt: "海をテーマにしたおススメ映画を教えてください",
    max_tokens: 4000
};

try{
  var result = await openai.createCompletion( option );
}catch( err ){
}

SDK を使って単純に createCompletion メソッドを実行しています。これだと 429 エラーが発生した時にそのまま 429 エラーを返すことになります。

ここを exponential backoff を実装して作り変えたものがこちらです(青字部分が変更箇所です):
var { Configuration, OpenAIApi } = require( 'openai' );
var configuration = new Configuration({ apiKey: "(API KEY)", organization: "" });
var openai = new OpenAIApi( configuration );
  :
  :

const wait = ( ms ) => new Promise( ( res ) => setTimeout( res, ms ) );
const progressingOperation = async ( option ) => {
  await wait( 10 );
  try{
    var result = await openai.createCompletion( option );
    return {
      status: true,
      result: result
    };
  }catch( e ){
    return {
      status: false,
      result: e
    };
  }
}
const callWithProgress = async ( fn, option, maxdepth = 7, depth = 0 ) => {
  const result = await fn( option );

  // check completion
  if( result.status ){
    // finished
    return result.result;
  }else{
    if( depth > maxdepth ){
      throw result;
    }
    await wait( Math.pow( 2, depth ) * 10 );
	
    return callWithProgress( fn, option, maxdepth, depth + 1 );
  }
}

var option = {
    model: "gpt-3.5-turbo",
    prompt: "海をテーマにしたおススメ映画を教えてください",
    max_tokens: 4000
};

try{
  //var result = await openai.createCompletion( option );
  var result = await callWithProgress( progressingOperation, option, 5 ); 
}catch( err ){
}


この実装ではデフォルトで最大7回まで再実行する想定で、失敗した後に(2 の試行回数乗) * 10 ミリ秒待ってから再実行する、という形で実装しています。


・・・ただ、OpenAI の無料枠の混雑具合は相当なものらしく、この方法で API を実行しても結局 429 エラーとなるだけっぽい気がしています。10 回くらい試してもいいのかな?

ネットにあまり情報がなく、自分で試行錯誤しながら色々調べてわかった備忘録的なネタです。

フロントエンドフレームワークではない、サーバーサイドアプリとしての Node.js で使える node-canvas というパッケージモジュールがあります:
https://www.npmjs.com/package/canvas

2023060100



これは HTML5 の Canvas 互換機能をサーバーサイドで使えるようになるモジュールです。UI のないサーバーサイドで HTML5 の Canvas ??と思う人がいるかもしれませんが、例えば画像を生成して返すような API を作ろうとした際に(仮想的な)Canvas を生成してグラフィックを描画し、最後に Canvas を画像化して(image/png などのコンテントタイプを指定して)出力する、といった使い方ができて便利です:
var { createCanvas } = require( 'canvas' );

  :
  :

app.get( '/image', function( req, res ){
// (仮想的な)Canvas を生成 var canvas = createCanvas( 200, 100 );

// グラフィックコンテキストを取得(ここから先は HTML5 の場合と同様) var ctx = canvas.getContext( '2d' );
// ctx に目的のグラフィックを描画していく
ctx.beginPath(); ctx.moveTo( 100, 0 ); ctx.lineTo( 200, 100 ); ctx.strokeStyle = 'red'; ctx.stroke(); : :
// 最後に Canvas の内容を 'imaga/png' で出力する res.contentType( 'image/png' ); var stream = canvas.createPNGStream(); stream.pipe( res ); });


便利なのですが、実はこの node-canvas は利用する上での大きな制約があります。システムにあらかじめ特定のライブラリがネイティブインストールされている必要があり、その前提でインストールすることによって使える、というものです。つまり普通にインストールしようとして "$ npm install canvas" と実行するだけでは正しくインストールできないことがあり、その前に必要なライブラリをインストールしておく必要があるのでした。例えば Ubuntu の場合は以下を実行しておく必要があります(他のシステムの場合はこちらの compiling 節を参照ください):
$ sudo apt install build-essensial libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev

さて、アプリを普通に動かすだけならこの(ドキュメントに書かれている)前提コマンドを知っているだけでいいのですが、Docker コンテナ上で動かすとなると少し事情が変わってきます。システム(= Docker ホスト)にあらかじめインストールしたライブラリは関係なく、Docker コンテナとして動かす、そのコンテナ上に上述の依存ライブラリを導入しておく必要があります。しかも Docker コンテナとして動かす場合は Dockerfile 内でベースイメージを指定することになるのですが、このベースイメージによっても(追加で)インストールが必要なライブラリが変わってきます。そういった複雑な事情がある中で、どのようにすれば Node.js で node-canvas が動くコンテナを作れるようになるか、というのがこのブログエントリの(自分の備忘録としての)目的です。もちろん自分以外で同様に Docker コンテナ内で node-canvas を使いたい人がいたとしたら、(特に日本語だとなかなかまとまった情報が見つからないので)以下の情報が有用であると思っています。

答としてはこんな感じです。node:16-alpine をベースとした場合のサンプルを記載します:
# base image
FROM node:16-alpine

# working directory
WORKDIR /usr/src/app

COPY package*.json ./

RUN apk update && apk upgrade && apk add python3 alpine-sdk

# apt install build-essensial libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev
RUN apk add build-base cairo-dev pango-dev libjpeg-turbo-dev giflib-dev librsvg-dev

RUN npm install

COPY . .

EXPOSE 8080
CMD ["node", "app.js"]


特に注目してほしいのは、上記 Dockerfile 内の赤字部分です。ここで "apt install build-essensial libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev" に相当するコマンドを実行して docker build 時の "RUN npm install" 実行直前に依存ライブラリをインストールしています。"npm install" でインストールする各モジュールを "apk add" 時にはどのように指定するか、1つ1つ調べる必要があり、その結果がこれでした。

この1行を追加しておくことで直後の "RUN npm install" (ここで node-canvas をインストール)も成功するようになり、Docker コンテナ内で node-canvas を使ったアプリケーションが期待通りに動くようになりました。


(補足)
"node-canvas" と互換性のある "@napi-rs/canvas" というモジュールが存在していて、こちらを使うとネイティブライブラリのインストールは不要です。絵文字などのフォントも内蔵しているようで、これはこれで便利そうです。

ただ上述のコードでは "canvas.createPNGStream()" を実行している箇所がありますが、この関数は @napi-rs/canvas では未実装らしくエラーになってしまいました。もしかするとこちらにも同様の互換関数があるのかもしれませんが未調査です。




このページのトップヘ