誰もついてこなかった頂上

その会社には、十年以上も放置された問題があった。

在庫は積み上がり、欠品は減らず、営業は「売れるものがない」と言い、製造は「読めない注文を出すな」と言い、経理は「数字が合わない」と言った。会議では毎回、誰かが深刻な顔で資料をめくり、最後には「引き続き検討しましょう」で終わった。

川瀬は、その空気が嫌いだった。

「できない理由を並べる時間があるなら、ひとつでも事実を見に行けばいい」

それが彼の信念だった。彼は優秀だった。誰よりも早く考え、誰よりも深く調べ、誰よりも諦めが悪かった。古い受注データを掘り返し、現場の棚卸表を集め、誰も見ていなかったシステムログまで読み込んだ。部署の境界を越えるたびに嫌な顔をされたが、彼は気にしなかった。

むしろ、そうした抵抗こそが、会社を腐らせてきたものだと思っていた。

半年後、川瀬は答えに辿り着いた。

問題は、単なる在庫管理ではなかった。営業目標、購買判断、製造計画、経理上の評価基準が、それぞれ別の理屈で動いていた。誰か一人が悪いのではない。会社全体が、矛盾したまま正しい顔をしていたのだ。

彼の提案は鋭かった。実行すれば、数億円規模の損失を止められる可能性があった。

最初に反応したのは、若手の森田だった。

森田は川瀬に憧れていた。会議で誰も言えないことを言い、曖昧な空気を一刀両断する姿は、停滞した会社の中で唯一まぶしく見えた。自分もああなりたいと思った。だが、川瀬の説明を聞くたびに、森田の胸には小さな不安が積もっていった。

結論は分かる。たぶん正しい。
でも、なぜそこまで言い切れるのかが分からない。
質問したい。けれど、そんなことも分からないのかと思われそうで、口を閉じた。

次に警戒したのは、課長の磯部だった。

磯部は変化を嫌っていたわけではない。ただ、組織の中で何かを動かす怖さを知っていた。正しい案でも、関係者の合意がなければ潰れる。現場が納得しなければ、数字は帳票の上だけで改善する。経営が一度乗っても、失敗した瞬間に責任の所在を探し始める。

川瀬の案は危うい、と磯部は思った。

内容ではない。進め方が危うい。
このままでは、彼自身も、周囲も傷つく。
だが、川瀬にそれを言えば、きっと「また調整ですか」と返される。磯部は何度か言葉を選び、結局、曖昧な懸念だけを口にした。

「もう少し丁寧に進めたほうがいいんじゃないか」

川瀬には、それが逃げに聞こえた。

現場主任の小野は、もっと早く拒絶した。

小野は三十年、倉庫と製造ラインを見てきた。どの商品が急に動くか、どの取引先が直前で変更してくるか、数字には出ない癖を肌で知っていた。川瀬の分析が間違っているとは思わなかった。むしろ、かなり当たっていると感じた。

だが、小野には許せないことがあった。

川瀬は現場に来た。質問もした。メモも取った。
けれど最後には、現場の言葉を「例外」「感覚」「属人性」として処理した。

小野は思った。

この人は、俺たちから材料を取っているだけだ。
一緒に考える気はない。
自分の答えを補強するために、現場を使っているだけだ。

それから小野は、必要最低限のことしか話さなくなった。

川瀬は苛立った。

なぜ分からないのか。
なぜ、会社を良くする話なのに、みんな自分の領分ばかり守るのか。
なぜ、ここまで調べた人間ではなく、何も決めなかった人間たちの顔色を見なければならないのか。

彼は会議で強くなっていった。

「その懸念は検証済みです」
「今その話に戻ると、また半年遅れます」
「感覚ではなく、数字で話してください」

ひとつひとつは間違っていなかった。だが、言われた側の心には、正しさではなく拒絶だけが残った。

森田は、川瀬の資料を読むのが怖くなった。理解できない自分が無能に思えたからだ。
磯部は、会議の前に根回しをするようになった。川瀬の案を通すためではない。暴発を防ぐためだった。
小野は、現場の細かな異常を上げなくなった。どうせまた数字の裏付けを求められるだけだと思った。

川瀬は孤立していった。

それでも彼は、まだ自分が正しいと思っていた。孤立は、改革者の宿命だと考えた。誰も理解しないなら、一人で進めるしかない。そう自分に言い聞かせた。

だが、ある日、決定的なことが起きた。

川瀬の提案をもとに試験運用した新しい発注ルールが、現場で止まった。データ上は問題ないはずだった。しかし、ある得意先だけが持つ特殊な納品条件が反映されておらず、結果として欠品が発生した。

小野は、その条件を知っていた。
森田も、過去の議事録で見かけていた。
磯部は、事前に現場確認が必要だと感じていた。

だが、誰も最後まで強く言わなかった。

「どうせ川瀬さんは聞かない」

それが、三人に共通した沈黙の理由だった。

責任を問う会議で、川瀬は初めて言葉に詰まった。ミスの原因はデータの不足だった。だが本当の原因は、必要な情報が彼のところに届かなくなっていたことだった。

会議の後、森田が小さな声で言った。

「川瀬さんの見ていた頂上は、たぶん本当に正しかったんだと思います」

川瀬は顔を上げた。

「でも、僕たちは道を知らなかったんです。途中で何を見ればいいのか、どこで声を上げればいいのか、分からなかった。ついて行きたくても、置いていかれるのが怖かった」

川瀬は反論しなかった。

その夜、彼は自分の資料を開いた。完璧だと思っていた分析には、結論があり、根拠があり、効果試算があった。だが、誰がどこで納得し、誰がどこで疑問を差し込めるのかは書かれていなかった。反対意見を受け止める余白もなかった。

それは道ではなかった。
頂上から撮った景色の写真だった。

翌月、プロジェクトは凍結された。正式には「再整理」とされたが、事実上の中止だった。川瀬は別部署へ移った。誰も彼を責めなかった。けれど、誰も引き止めなかった。

最後の日、磯部が言った。

「君の言っていたことは、たぶん間違っていなかった」

川瀬は苦笑した。

「それが一番きついですね」

正しければ届くと思っていた。
強くあれば変えられると思っていた。
だが、組織における正しさは、一人の頭の中にあるだけでは足りなかった。人がそこへ近づける順路があり、迷ったときに戻れる足場があり、自分も関われたと思える余地がなければ、正しさはやがて命令に見える。

川瀬が去った後、彼の資料の一部は残った。森田は時々それを開いた。そこには今でも、有効な分析がいくつもあった。ただし、森田はそれをそのまま使わなかった。

最初のページに、彼は一行だけ書き足した。

「この資料は、結論ではなく、対話の出発点として扱うこと」

それを川瀬が読むことはなかった。

会社はすぐには変わらなかった。会議は相変わらず長く、判断は遅く、問題の多くは残ったままだった。

ただ、かつて誰よりも早く頂上に立った男の背中を思い出す者はいた。
そして同時に、その背中があまりに遠く、誰も追いつけなかったことも覚えていた。

道をひらく者は、先に進むだけでは足りない。
後ろを振り返らない正しさは、いつか孤独になり、孤独になった正しさは、組織の中で信頼を失っていく。

 

本稿は、生成AI(OpenAI GPT-5.5 Thinking)によって生成された文章です。

アジャイル開発とスクラム第2版を読んだ感想

www.agile-studio.jp

こちらのブログをみて興味が湧き、1日で読んだ感想と特に感銘を受けた部分について

 

第2部にて各企業のアジャイルを実践している方の体験記が書かれているが、どの情熱がなければ進められなかったであろう内容が書かれているのがとてもよかった。

自分は昔受託開発企業で働いていて、ユーザー企業のユーザー部門とシステム部門、受託会社の間で行われる駆け引きというか・・・総体としてやりたい思いは同じであろうに結局やりたいことがやれないジレンマに陥るのを体験したことがあり、それを乗り越えていった軌跡がどれもよかった。

 

野中さんとの対談もとてもよかった。

イノベーションというのは思いから生まれると、まず最初に情熱がなければいけないというのは20年と少しこの業界に関わってきてやっとわかってきたことでもあったので、勇気づけられる対談になっていた。

 

トップ、ミドル、フロントの役割に触れている部分もよかった。ミドルが上下に動くことで会社全体がひとつになっていけるのだろう、というのは実感がある。もちろんフロントからも支援が必要だろう。

実践知リーダーを増やしていくこと、縦横の壁を取り払って本音で語り合うというのは実践していきたいところ。難しいことではあるけど。

 

RailsのActiveSupport::Cacheの実装とデータの保存形式について調べてみた

これは Ruby on Rails その2 Advent Calendar 2018 - Qiita20日目の記事です。

※ 実は PeXのRailsを2年ぶりに4.2から5.0にアップデートしました。キャッシュやセッションまわりでのエラーにご注意! - VOYAGE GROUP techlog のキャッシュでハマったことの補足にもなっています。よかったらこちらも見ていただけると嬉しいです!

Railsアプリケーションではキャッシュ機能を利用する時 ActiveSupport::Cache を使って実装すると思います。アプリケーションから利用する時は Rails.cache.writeRails.cache.read ですね。 DBへ重いクエリを投げるところの負荷をさげるため、クエリ結果であるActiveRecordをキャッシュするというのが最も多い利用方法かなと思います。

そのキャッシュ部分について ActiveSupport::Cache の内部実装について踏み込んで見ていきます!

キャッシュにどのように格納されるのか?

他の言語やフレームワークでキャッシュを利用したことがある方であれば、何かしらシリアライズされたフォーマットでRedisなりMemcachedなどのキャッシュストアに保存されるのだろうな、というのは想像出来ることだと思います。

ActiveSupport::Cache で保存するときは Marshal を使ってマーシャルデータがキャッシュに保管されます。

マーシャルデータの構造については詳しく触れないのですが、Marshal フォーマット (Ruby 2.5.0) を見ると、ふむふむよくわからんってなると思います。

雑に説明するとActiveRecordのようなgemで定義されているクラスは「instance of the user class」の「上記以外では、'o' になる。」にあたり、シリアライズされたデータそのものにクラス名を含みます(←重要)。

実際にコードを見てみる

キャッシュストアとしてRedisを使う場合のコードを追ってみます。

Rails4.2〜5.1時点ではredis-storeはRailsに取り込まれておらず、redis-rails を使うことになります。gem内部で redis-store redis を使っているのでそのコードを追ってみます。

以下が実際にシリアライズしている部分です。 https://github.com/redis-store/redis-store/blob/v1.6.0/lib/redis/store/serialization.rb

      def set(key, value, options = nil)
        _marshal(value, options) { |v| super encode(key), encode(v), options }
      end

...snip...

      private
        def _marshal(val, options)
          yield marshal?(options) ? @serializer.dump(val) : val
        end

@serializer というのはなんでしょう。 redis を継承してるのでそちらも見ます。

https://github.com/redis-store/redis-store/blob/614d79c80f25a78601277176d0ed7a148f11a636/lib/redis/store.rb#L30

      @serializer = options.key?(:serializer) ? options[:serializer] : Marshal

@serializer はデフォルトでは Marshal になるようです。つまり、 Marshal.dump の内容が格納されることになります。

Rails5.2ではredis storeが標準に取り込まれていますが、そこでも実装は同じようです。

https://github.com/rails/rails/blob/5-2-stable/activesupport/lib/active_support/cache/redis_cache_store.rb#L431-L437

        def serialize_entry(entry, raw: false)
          if raw
            entry.value.to_s
          else
            Marshal.dump(entry)
          end
        end

実際に格納されているデータを見てみる

Rails5.2の環境で、こんな属性を持った簡単なActiveRecordインスタンスをキャッシュしてみます。

#  id         :integer          not null, primary key
#  code       :string(255)      not null
#  name       :string(255)      not null
#  created_at :datetime         not null
#  updated_at :datetime         not null

こんなデータです。

[4] pry(main)> category = Category.last

+----+------+------+---------------------------+---------------------------+
| id | code | name | created_at                | updated_at                |
+----+------+------+---------------------------+---------------------------+
| 4  | news | ニュース | 2018-12-14 18:12:54 +0900 | 2018-12-14 18:12:54 +0900 |
+----+------+------+---------------------------+---------------------------+
1 row in set

このActiveRecordインスタンスMarshal.dump した中身を見てみましょう。(見づらいので適宜改行をいれています

[6] pry(main)> Marshal.dump category
=> "\x04\bo:\rCategory\x0F:\x10@attributes
o:\x1EActiveModel::AttributeSet\x06;\x06U:#ActiveModel::LazyAttributeHash[\n}\nI\"\aid\x06:\x06ET
o:\x1FActiveModel::Type::Integer\t:\x0F@precision0:\v@scale0:\v@limiti\t:\v@range
o:\nRange\b:\texclT:\nbeginl-\a\x00\x00\x00\x80:\bendl+\a\x00\x00\x00\x80I\"\tcode\x06;\tT
o:HActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlString\b;\v0;\f0;\ri\x01\xFFI\"\tname\x06;\tT@\x10I\"\x0Fcreated_at\x06;\tTU:JActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter[\t:\v__v2__[\x00[\x00
o:!ActiveRecord::Type::DateTime\b;\vi\x00;\f0;\r0I\"\x0Fupdated_at\x06;\tTU;\x14[\t;\x15[\x00[\x00@\x17
o:\x1DActiveModel::Type::Value\b;\v0;\f0;\r0{\nI\"\aid\x06;\tTi\tI\"\tcode\x06;\tTI\"\tnews\x06;\tTI\"\tname\x06;\tTI\"\x11\xE3\x83\x8B\xE3\x83\xA5\xE3\x83\xBC\xE3\x82\xB9\x06;\tTI\"\x0Fcreated_at\x06;\tTIu:\tTime\r\xC9\xAD\x1D\xC0\x00\x00`3\x06:\tzoneI\"\bUTC\x06;\tFI\"\x0Fupdated_at\x06;\tTIu;\x18\r\xC9\xAD\x1D\xC0\x00\x00`3\x06;\x19I\"\bUTC\x06;\tF{\x00{\x06@\n
o:)ActiveModel::Attribute::FromDatabase\t:\n@name@\n:\x1C@value_before_type_cast0:\n@type@\v:\x18@original_attribute0{\n@\no;\x1A\n;\e@\n;\x1Ci\t;\x1D@\v;\x1E0:\v@valuei\t@\x0Fo;\x1A\n;\e@\x0F;\x1C@!;\x1D@\x10;\x1E0;\x1FI\"\tnews\x06;\tT@\x11o;\x1A\n;\e@\x11;\x1C@#;\x1D@\x10;\x1E0;\x1FI\"\x11\xE3\x83\x8B\xE3\x83\xA5\xE3\x83\xBC\xE3\x82\xB9\x06;\tT@\x12o;\x1A\n;\e@\x12;\x1C@&;\x1D@\x13;\x1E0;\x1FU: ActiveSupport::TimeWithZone[\bIu;\x18\r\xC9\xAD\x1D\xC0\x00\x00`3\x06;\x19I\"\bUTC\x06;\tFI\"\nTokyo\x06;\tTIu;\x18\r\xD2\xAD\x1D\xC0\x00\x00`3\x06;\x19I\"\bUTC\x06;\tF@\x18o;\x1A\n;\e@\x18;\x1C@);\x1D@\x19;\x1E0;\x1FU; [\bIu;\x18\r\xC9\xAD\x1D\xC0\x00\x00`3\x06;\x19I\"\bUTC\x06;\tF@8Iu;\x18\r\xD2\xAD\x1D\xC0\x00\x00`3\x06;\x19I\"\bUTC\x06;\tF:\x17@aggregation_cache{\x00:\x17@association_cache{\x00:\x0E@readonlyF:\x0F@destroyedF:\x1C@marked_for_destructionF:\x1E@destroyed_by_association0:\x10@new_recordF:\x1E@_start_transaction_state{\x00:\x17@transaction_state0"

先に見た、マーシャルデータのフォーマットと照らし合わせて雰囲気で見てみると ActiveModel::Attribute::FromDatabase だとか ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter というクラス名が見えてくると思います。 ActiveRecordインスタンス以外にも、attiributeとして保持している関連オブジェクトのインスタンスのオブジェクトも一緒に格納されていて、キャッシュからロードしてオブジェクトを復元するときは、ここに文字列として保存されているクラス名をもとに各オブジェクトが復元されます。

オブジェクトをキャッシュから復元する時の問題と回避策

マーシャルデータを復元するときに、そこに書かれているクラス名が存在しない環境でロードすると、クラスが見つからなくて当然エラーになります。

どういうときに起こるかというと例えばgemのバージョンアップを行った前後で内部クラスの構造が変化すると、クラス名が変わることはありえます。 具体的には、Railsのマイナーバージョンのアップデートを行うと、active_record以下のクラス構造が変更されていることはよくあるようです。 つまりいまのバージョンでは ActiveRecord::AttributeMethods::TimeZoneConversion::TimeZoneConverter というクラスがあるけど、次のバージョンでは別の名前になってる、ということがありえます。

しかし、ActiveRecordをキャッシュしたい要件としては、DBへの読み込み回数を抑えるのが目的なので、ActiveRecordそのものをキャッシュしないようにする、というのは難しいシーンも有るかと思います。

この場合の回避策としては、キャッシュのネームスペースを変えてしまって、完全にバージョン間で違うキャッシュ空間を利用するようにする、というのが回避策になるかと思います。 これをやるためには、キャッシュが吹っ飛んだ瞬間DBが耐えられなくなってサービスダウンしてしまう、ということがないように適切にインデックスを張っておくなど、普段からパフォーマンスにも気を使っておくのが必要となります。

別の対応策としてはキャッシュのTTLよりも長い時間サービスを停止してリリースするという方法もあるかと思いますが、サービス断を伴う場合はビジネス的にもユーザ的にも配慮が必要になり調整等に時間が取られてしまうので、できればサービス断をせずにバージョンアップできる方法をとりたいですよね。

さいごに

キャッシュってすごく便利で今日のウェブサービスで使わないことはほとんど無いと思うんですが、実際に中に何が入っててどういう問題が起きうるの?というところを調べて見たので今日はその点について書いてみました。参考になれば幸いです。

fetch_mldata('MNIST original') が出来ない

from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original')

 

とやっても mldata.org が落ちてて全然ダウンロードできないんだけどぉっていう時

mldata.org is down (for good?) · Issue #8588 · scikit-learn/scikit-learn · GitHub

を参考に mnist-original.mat を直接ダウンロードして ~/scikit_learn_data/mldata/ に置けばOK

このディレクトリがキャッシュとして利用されるので fetch_mldata でデータセットが読み込めます。