rstdという趣味ライブラリを作っています

はじめに

この記事は Ruby Advent Calendar2 2021の12/6の記事です。

qiita.com

二年くらい前から趣味で作っているrstdというライブラリの紹介したいと思い、参加しました。

作っているもの

rtsdという「標準ライブラリにあると便利そうなメソッド」をRefinementsを使って実装しているライブラリを作っています。

github.com

きっかけ

Array#both_endというメソッドをbugs.ruby-langで提案したのが切っ掛けで作り始めました。

bugs.ruby-lang.org

挙動としては以下のような感じで動くと良いのではと考えてました。

# 通常のArray
ary = [ "w", "x", "y", "z" ]
ary.both_end     #=> ["w", "z"]
ary.both_end(2)  #=> [["w", "x"], ["y", "z"]

# 空のArray
[].both_end    #=> [nil, nil]
[].both_end    #=> [nil, nil]

提案したところコミッターの卜部さんから「Why not start as a gem? There are things not possible without core changes, but it seems this requested feature needs no such things.」とコメントがあり、gemで実装もできるしやってみるかとなりrtsdを作り始めました。

モチベーションなど

基本的に気の向いたときに実装をしたり、CIのメンテナンスしたりとかな感じです。 なので、一時コミットが全くなかったり……。

最近は少しずつ暇を見つけてはアレコレ便利そうなメソッドを追加したりしてます。

最終的にどういうものにしたいとか明確な目標はないですが、少なくとも「自分が使ってて便利そうなメソッドを追加する」という方針だけは維持してゆるく作りたいと思ってます。

使い方

rtsdは以下のようにrequireした後に、使用したいメソッドを定義しているModuleusingするだけです。

require "rstd"

using Rstd::RefineInteger

41.inc
# => 42

個人的に気に入っているメソッドなど

Array#has?

Array#has?は以下のような配列内に引数で渡した値があるかをチェックしてくれるメソッドです。 既に標準ライブラリではArray#include?がありますが、もう少し短く書きたかったので追加した感じです。

require 'rtsd'

using Rstd::RefineArray

ary = [1, 2, 3, 4, 5]

ary.has?(1)
# => true

ary.has?(117)
# => false

Hash#has_pair?

Hash#has_pair?は特定のキーとバリューの組み合わせを持っているかをチェックするメソッドですね。 Hash#has_key?Hash#has_value?を組み合わせて書いても良いんですが、毎回書くのも面倒だなと思い、追加しています。

require 'rstd'

using Rstd::RefineHash

hash = {jhon: 'S-117', linda: 'S-058', fred: 'S-104', kelly: 'S-104'}

hash.has_pair?(:jhon, 'S-117')
# => true

has.has_pair?(:daisy, 'S-023')
# => false

Kernel.#refine_method

Kernel.#refine_methodはクラス内でブロックとRefinementsを使って、良い感じにその場でメソッドを追加したいので実装してみました。

require 'rstd'

using Rstd::RefineKernel

using refine_method(Integer, :halo) do
  p 'Master Chief is came back!'
end

42.halo
# => 'Master Chief is came back!'

こんな感じで書けるので個人的には一番良く使っています。

おわりに

簡単ですが、趣味で作っているライブラリを紹介させていただきました。 今後、色々便利メソッドとか実装してみて標準にもあると嬉しいなというものがあれば提案していきたいと思います。

島根県浜田市での暮らしなど

はじめに

この記事は地方在住 Advent Calendar 2021の12/5の記事です。

僕も地方在住でリモートで仕事をしているので日々の生活などを振り返ってみようかと思い参加しました。

 

居住地

現在は島根県浜田市に住んでいます。もともと出身も島根県西部のほうなので生まれてこの方島根から出たことがないのが自慢です。

 

浜田市に住むきっかけは市内にある大学に進学して、そのままそこの地元企業に就職したためですね。

で、以下の記事でも触れていますが、社会人になってから本格的にプログラミング学んで現職に転職してます

 

gamelinks007.hatenablog.com

 

 

仕事など

リモートで東京のメンバーと一緒に仕事しています。

現在はCRE(Customer Reliability Engineering: 顧客信頼性エンジニアリング)というロールで顧客満足度改善のための改修などをメインにGoやTypeScript(React)で開発をしています。

 

あとは島根県浜田市の地域.rbであるHamada.rbを主催してたり、浜田市若者会議という市への提言をするグループとかに参加してITを使った街づくりとか提案したりしてます。

 

gamelinks007.hatenablog.com

 

 

gamelinks007.hatenablog.com

 

日々の暮らしなど

島根県浜田市特定第3種漁港もあるので、新鮮な魚を普段から安く買えるのがうれしいですね。たまにスーパーとかで丸ごと一尾買って自分で捌いたりもしてます。

 

特に市としても売り出しているどんちっち三魚(アジ、ノドグロ、カレイ)は非常に美味です。

 

個人的にオススメなのは「めし処ぐっさん」というところが提供しているノドグロの炙り丼です。

 

f:id:gamelinks007:20211204232244p:plain



脂たっぷりで非常に美味しいので浜田市に来られる際にはぜひ試してみてください。

 

 

www.kankou-hamada.org

 

あと、浜田市は日本酒の酒蔵もあり、美味しい海鮮と合わせて日本酒も楽しめるのでお酒好きの方にもおすすめです。

 

shimane-sake.or.jp

 

あと島根自体は酒蔵も多く、天隠とか七冠馬とか玉鋼など美味しい日本酒も多いので最近はその辺を晩酌で楽しんでたりします。

 

それと新型コロナウイルスの影響で行けていませんが、温泉街もあるのでそこへたまに疲れをいやしに行ってたりもしました。

 

www.kankou-hamada.org

 

PCで長時間コード書いたりしてると肩こりとかも酷くなるので、温泉に気軽に行けるのは助かってました。

最近行けてないので、どこかのタイミングで行きたいなぁ……。

 

おわりに

とりあえず、雑に書いてますが現状浜田市での生活に大きな不満はないですね。

美味しいご飯と日本酒とか楽しめてて、温泉とかもあるのでだいぶ楽しく生活してます。

 

少しだけ難点があるとすると映画館で映画を見るのに広島か出雲まで行かないといけないのは辛いですね……。

そこらへんが解消されたら嬉しいですが、どうだろうなぁ……。

近況報告という名の振り返り

はじめに

この記事はRubyist近況 Advent Calendar2021(2スレ目)の12/3の記事です。

近況報告の体で今年一年を振り返ってみようかなと思い、参加してみました。

 

この一年でやってたこと

お仕事

 

お仕事のほうではCREというポジションに就き、Go/TypeScriptを使い自社サービスの改善などをガンガン進めることになりました。

 

ちなみにCREについては以下の記事が詳しいです。

engineering.mercari.com

 

で、お仕事として以下のようなことをやってたりしました。

 

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

 

基本的にGo/TypeScriptばかりですが、Zoomの動画を文字起こしするのだけは個人的な趣味でRubyを使ってたりします。

 

あと最近はリリースの対応とかを率先してやるようにしていて、改善のサイクルをガンガン回せるように気を付けてたりします。

 

浜田市での活動とか

現在、島根県浜田市に住みつつリモートで仕事をしています。

で、隙間時間とかを使って高校生にプログラマーの仕事について話したり、便利そうなLINE botとかを作ってたりしてます。

 

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

gamelinks007.hatenablog.com

 

CRubyへのPRとか

去年もCRubyに色々PR投げたりしてたんですが、今年はよりアレコレ投げてた感じですね。

コミットなどは以下にまとまってます。

 

github.com

 

個人的には以下の「RubyRuby自体を実装してパフォーマンスが向上した」系のPRがマージされたのがうれしいですね。

 

github.com

github.com

github.com

 

来年もゆるく頑張っていきたいと思います。

Hamada.rb

Hamada.rbという地域.rbのオーガナイザーもやってまして、今年で無事丸二年たちました。

hamadarb.connpass.com

 

ちなみに今確認したらRuby Hacking Challenge in Hamada.rbもはじめてから丸二年たってたみたいです。

こちらはCRubyの内部実装読んだり、最近出たチケットの話とかをしてます。

 

元々はクックパッドで開催されていたRuby Hack Challenge の資料とか使わせてもらいつつ、開催してました。

rhc.connpass.com

 

ある時から、CRubyの実装を読んだりしてる人が集まる感じになってきたのでCRubyの実装を読んだりする感じにシフトしていった感じですね。

ちなみに、今年Rubyに投げたPRとかもこのイベントの時に作ってたりしました。

 

今後の目標としては浜田Ruby会議とかやりたいなと思ってます。

 

おわりに

近況報告という体で一年振り返ってみたんですが、まあアレコレやってたなという印象。

来年も引き続きやっていきたいですね。

浜田市への意見などを投稿できる目安箱サービスを試しに作ってみた

はじめに

元々は浜田市若者会議という会で、「浜田市に住んでいる若者がどんな意見を持っているかを知りたい」という話が出たのがきっかけです。 とりあえず、作ってはみたんですがまだ実際にリリースしたりするように話が詰まっていないので、本当にリリースするかは未定です。

ちなみに、浜田市若者会議についてはこちらの記事を参照していただければと思います。

gamelinks007.hatenablog.com

で、色々とアイディアとか仕様とかまとめたところを「目安箱のようなサービスでいいんじゃないか?」と思い、この土日を使って実際に作ってみました。

作ったもの

github.com

f:id:gamelinks007:20211115222921p:plain

Firebase/React/TypeScriptを使い、開発しました。

投稿されたご意見はFirebaseのRealtime DBに保存され、保存されるタイミングでCloud Functionsに追加した処理が呼び出され、Slackに自動で転送されるようになっています。

やったこと

基本的にはコミットログを見ていただければわかりますが、Firebase周りの設定を整えたりBlumaを使っていい感じにコンポーネントを作ったりしています。 あと、フォーム周りは react-bulma-componentsbulma-toastを使ってシンプルに実装しています。

なので、全体的にあまり時間はかかってない感じですね。

あとは、Slackへの転送する処理とかを以下のような感じで実装してます。

github.com

ちなみに、問い合わせなども同じようにSlackに流すようにしています。

おわりに

とりあえず、サクッとFirebaseを使ってシンプルな目安箱サービスを作れた! あとは実際にサービスとして提供できるように諸々の話を詰めていきたいですね

GoでWebsocketを使ったリアルタイムチャットを作ってみた

はじめに

勉強がてらGoでWebsocketを使ったチャットアプリを作ってみた時の備忘録です。

作ったもの

github.com

フレームワークとしてはGinを使い、ORMにはgorm v2を使っています。 またWebsocket周りはmelodyを使っています。

github.com

やったこと

細かいところは基本的にはコミットログをにまとまっているのでそちらを見ていただければと思います。

github.com

実装の肝としては、routes/setup.goに以下のようにWebsocket用のルーティングを追加して、

   // チャットルーム用のWebsocket接続先
    router.GET("/rooms/:id/messages/ws", messageController.Websocket)

    m.HandleMessage(func(s *melody.Session, msg []byte) {
        m.Broadcast(msg)
    })

    m.HandleConnect(func(s *melody.Session) {
        log.Printf("websocket connection open. [session: %#v]\n", s)
    })

    m.HandleDisconnect(func(s *melody.Session) {
        log.Printf("websocket connection close. [session: %#v]\n", s)
    })

    // メッセージがブロードキャストされたときの処理
    m.HandleMessage(messageController.Broadcast)

フロント側から接続するときのルーティングとWebsocket周りの接続を設定しています。

あとは、メッセージを作成する処理をコントローラーのアクションを以下のように作成しています。

func (messageController *messageController) Broadcast(s *melody.Session, msg []byte) {

    roomPath := strings.Trim(s.Request.URL.Path, "/roomsmessagews")
    roomID, _ := strconv.ParseUint(roomPath, 10, 64)
    var message model.Message
    message.Content = fmt.Sprintf("%s", msg)
    message.RoomID = roomID
    messageController.messageRepository.Store(messageController.db, &message)

    messageController.m.BroadcastFilter(msg, func(q *melody.Session) bool {
        return q.Request.URL.Path == s.Request.URL.Path
    })
}

あとはフロント側でWebsocketのエンドポイントに接続させておけばいい感じです。

やってみて

意外と簡単に実装できたので今後仕事のコードとかで導入してみてもいいかなと思いました。

gormでPreload周りのメモリ使用量とかを比較してみた

はじめに

gamelinks007.hatenablog.com

この記事は、上記の記事でgormのバージョンを上げる際にPreload周りの挙動を調べてた時のあれこれをまとめたものです。

ことの発端

gorm v2へのアップデート時に、一部処理でメモリリークらしき挙動が確認されたのがことの発端。 で、色々調べてた感じでgorm v2でPrelaod関連でmap[string]interface{}がそこそこ使われてたのでこの辺でメモリリークしてるのかもと思いPreload周りのメモリ使用量を調査してみました。

ちなみに、メモリリークしてたのは変なクエリをコード内で作成してて、それが原因でテーブル内のレコードを全件取得してたのが原因でした……。

前提条件

gorm v1とv2との間でPreloadを多用した際にメモリの使用量に差が出るのかを調査します。

メモリ使用量などはPrometheusやGrafanaを使いグラフで監視することにしました。

また最低限度の実装でも再現するのかを確認したいので使用するライブラリは以下のものだけにしています。

  • gin
  • gorm(v1またはv2)
  • sqlite3

またベンチマークにはApache Benchを使用し、ベンチマーク用のプログラムとしては以下を使用しました。

またデータの数も影響する可能性を考えてデータは以下のようなものを使用した。

  • users
    • 件数:10000
    • テーブル構成:create table users(id integer primary_key, name text not null);
  • tweets
    • 件数:10000
    • テーブル構成:create table tweets(id integer primary_key, content text not null, user_id integer);

なお、tweetsのuser_idはランダムにしているためv1とv2とで取得する件数は若干違います(ただしその差は2件のみ)

調査結果

f:id:gamelinks007:20211027215459p:plain

以下のリクエストをそれぞれのエンドポイントに投げたグラフが上のものです。

ab -n 10000 -c 50 <エンドポイント>

なお、v1とv2それぞれのリクエスト成功数は以下の通りです

v1: Total of 3305 requests completed(17:49に開始)
v2: Total of 5588 requests completed(17:50に開始)

傾向としては - v1 - 全体的にヒープ上のメモリを使う - ヒープのオブジェクト数はv2より少ない - GCの呼び出し回数はv2より多い - v2 - 全体的にヒープ上のメモリはv1より使用しない - ヒープ上のオブジェクト数はv1に比べてかなり多い - GCの呼び出し回数はv1より少ない

総合するとv2のほうが総合的なパフォーマンスは上っぽい。リクエストをさばいた量も多く、且つヒープ上のメモリもv1に比べると少ないのも評価できるポイント。

ただv2はGC自体の呼び出し回数が少ないので他のクエリも同時に捌いている状態だとパフォーマンスが悪くなる可能性があるかもくらい……?

おわりに

トータルで見るとgorm v2のほうがパフォーマンスが上なので、これからGoでWebアプリ開発とかするのであればgorm v2使うほうが良さそう。

gormのアップデートをした話

はじめに

この記事は、仕事で使っているgormのバージョンをv1からv2にアップデートした時の備忘録です。 またgorm v1を使っている人でかつv2にアップデートしたい人向けの記事なので細かいところは省いています。

経緯

仕事で使っているgormのバージョンがv1で特に問題もなく開発できていたんですが、今後の開発で「v2に上げたほうがいいんじゃないか」という話になりアップデートを進めることになりました。 で、ORMのアップデートとかいい経験になりそうだったので手を挙げてみた感じです。

やったこと

v1とv2の差分確認

まずは、ざっと公式のリリースノートを読みながら変更箇所の把握を進めました。

gorm.io

次に、実際にv2へとアップデートした先人の知見などを確認しておおよその変更箇所を確認していきました。

tech.techtouch.jp

qiita.com

future-architect.github.io

zenn.dev

tchssk.hatenablog.com

大体上記の記事を読めば、おおよその変更差分は確認できますね。

使用するパッケージの置き換え

一番最初に進めたのは gorm v1 を使っている箇所をv2に置き換えることでした。

import (
-     "gorm.io/gorm"
+    "github.com/jinzhu/gorm"
)

パッケージを置き換えることでビルドエラーになっているところがはっきりし、そこを随時直していけば手戻りも少なく済みそうだったので。

DBのドライバー置き換え

次にDBのドライバーを置き換えました。

gorm v2ではv1の時とDBへの接続方法が変わっています。

gorm v1では以下のようなコードで接続できます。

import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
  db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
  defer db.Close()
}

ただ、v2ではこの辺りの仕様が変わっているので以下のように置き換える必要があります。

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

RecordNotFoundの置き換え

次に、RecordNotFoundエラーの置き換えを進めました。

gorm v1では以下のようにレコードがない場合のエラー処理を行います。

if err := db.Where("name = ?", "jinzhu").First(&user).Error; gorm.IsRecordNotFoundError(err) {
  // record not found
}

この辺りの処理はv2ではerrorsを使ってチェックする方式に変わっています。

errors.Is(err, gorm.ErrRecordNotFound)

サブクエリの置き換え

gorm v1ではサブクエリは以下のようにSubQueryを使っていました。

db.Where("amount > ?", db.Table("orders").Select("AVG(amount)").Where("state = ?", "paid").SubQuery()).Find(&orders)

これがgorm v2ではSubQueryメソッドが廃止され、以下のようにクエリを引数として渡すように変更されています。

subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)

型エラーの修正

gorm v2ではLimit()Offset()が受け取る引数の型がuint64からintに変更されています。

その為、既存の実装を以下のようにキャストして対応しました。

db.Limit(int(limit)).Offset(int(offset))

またCount()が受け取る引数の型もint64に変更されているので以下のようにキャストしてたりします。

var total int64
db.Where("user_id = ?", userID).Count(total)
return uint64(total)

本来であれば元々の引数の型を修正するのが筋がいいんですが、今回は比較的急ぐ必要もあったのでキャストすることで対応しています。 この辺りは今後も改修が必要そうです。

おわりに

基本的にこんな感じの置き換えなどをガガガっと進めたのでアップデートに着手してから大体1か月くらいでリリースまで行けたかな? 変なクエリとか書いて無ければ大体上記に書かれている内容に置き換えていけばすんなりアップデートはできそう。