Uploaded image for project: 'Mongoid'
  1. Mongoid
  2. MONGOID-5819

Mongoid 9.x incompatibility with store_in procs

    • Type: Icon: Bug Bug
    • Resolution: Unresolved
    • Priority: Icon: Critical - P2 Critical - P2
    • 9.0.3
    • Affects Version/s: None
    • Component/s: Persistence
    • None
    • Ruby Drivers

      Mongoid 8.x to 9.x incompatibility with `store_in`

      We have come across an incompatibility between Mongoid 8 and Mongoid 9 that causes our application to not work under Mongoid 9.x. This is preventing us from upgrading to the latest Mongoid line for our Ruby on Rails applications. We believe this is a bug in the 9.x release line of Mongoid.

      Specifics versions we are using:

      • Ruby v 3.3.1
      • Rails v 7.1.4
      • mongoid v 8.1.5, works
      • mongoid v 9.0.2, breaks
      • MongoDB Atlas current in production clusters, testing against docker/podman `mongo:latest`
      • OS: reproduces consistently on all of MacOS 14.7, Ubuntu 2022 LTS and RedHat UBI

      Our application leverages Mongoid's `store_in` to drive runtime persistence options of our document models, leveraging procs to return values to the `store_in` configuration. We think the incompatibility is specific to `store_in`. We specifically make use of the `store_in` hooks for `database:` and `collection:` extensively.

      Our code uses a `proc` passed to the 

      store_in collection: -> {...} 

      declaration that makes use of a value in the current executing Ruby thread, using `Thread.current[...]` to retrieve the name of the MongoDB collection to operate against. Under Mongoid v8.x this capability worked correctly, dynamically evaluating the `proc` at run-time. Under Mongoid v9.x it appears that the Gem is attempting to cache or is not evaluating the proc for `collection:` before interacting with MongoDB.

      Context and Background

      Our application has a model class called `Widget` that persists into multiple MongoDB collections. Widgets have several properties, one of which is a `type`. A Widget's `type` is simply a UUIDv4 string.

      We persist the widget documents twice in order to support multiple indexing strategies. Specific widget types receive different indexes than the main `widgets` collection. In our system each `Widget` document is persisted with two copies: one copy in the `widgets` collection, and one copy in the `widgets_of_type_$uuid` collection.

      In practice our customers tell us the properties of the different types of Widgets and our application chooses to query either the `widgets` or the `widgets_of_type_...` collection based on the nature of the incoming request. In general if a widget's type is in context, queries are directed to the `widgets_of_type_...` collection instead of the `widgets` collection. Our app externally manages index declarations on the `widgets_of_type_...` collections and that is unrelated to reproducing the issue. This all works well in production today on Mongoid 8.x.

      As data changes, we use call back hooks on `after_save`, `after_upsert` and `after_destory` to replicate document changes from the `widgets` collection to the `widgets_of_type_$uuid` collections. This is where our use of `proc`s for the `store_in` configuration come into play.

      The attached `widget.rb` ruby class illustrates a simplified `Widget` definition and use that can reproduce the issue.

      widget.rb

      Issue

      Under Mongoid 8.x the call backs that upsert or delete the second copy of a widget works.

      Under Mongoid 9.x the evaluation of `store_in` and call backs fail with the following error:

      ```
      (irb):13:in `<main>': [73:InvalidNamespace]: '.' is an invalid character in a db name: #<Proc:0x000000012fc99b60 /Users/adam.hampton/../app/models/widget.rb:13 (lambda)> (on localhost:27017, legacy retry, attempt 1) (Mongo::Error::OperationFailure)
      ```

      Reproduction Steps

      The reproduction process can be validated against Mongoid 8.x, then the issue reproduced against an app on Mongoid 9.x. Start with an app with Mongoid 8.x. Take the class definition from the Ruby snipped above and place it in a Rails + Mongoid project.

      1. Initialize the class and validate it at a Rails or `irb` console:

      ```ruby
      irb(main):002> Widget.create_indexes
      irb(main):003> Widget.count
      => 0
      ```

      2. At a Rails console declare, populate, persist and count some widgets:

       
      widget_type_1 = SecureRandom.uuid
      widget_type_2 = SecureRandom.uuidwidget_1 = Widget.new(
      {{id: SecureRandom.uuid, }}
      {{name: "Widget No. 1", }}
      widget_type_id: widget_type_1
      )
      widget_1.valid?
      widget_1.save

      1. => true

      widget_2 = Widget.new(
      {{id: SecureRandom.uuid, }}
      {{name: "Widget No. 2", }}
      widget_type_id: widget_type_2
      )
      widget_2.valid?
      widget_2.save# => trueirb(main):023> Widget.count# => 2irb(main):024> Widget.with_widget_type_collection(widget_type_1){{{}

      { Widget.count }

      {}}}
      # => 1

      irb(main):025> Widget.with_widget_type_collection(widget_type_2) { Widget.count }{}# => 1
       

      When running against `mongoid (9.0.2)` we experience a failure attempting to persist the first Widget.

      ```
      irb(main):011> widget_1 = Widget.new(id: SecureRandom.uuid, name: "Widget No. 1", widget_type_id: widget_type_1)
      irb(main):012> widget_1.valid?# => trueirb(main):013> widget_1.save
      (irb):13:in `<main>': [73:InvalidNamespace]: '.' is an invalid character in a db name: #<Proc:0x000000012fc99b60 /Users/adam.hampton/.../app/models/widget.rb:13 (lambda)> (on localhost:27017, legacy retry, attempt 1) (Mongo::Error::OperationFailure)
      ```

      As best we can tell, the evaluation of the proc for `store_in` under Mongoid v9.x is failing to evaluate the proc and is instead interpreting this as a string literal:

      ```
      collection: ->{{{}

      { Thread.current[:widget_type_collection] || "widgets" }

      {}}}

      ```

      We think the dot `.` character comes from the `Thread.current` being interpolated as the target collection name instead of being evaluated as a Ruby proc. Rolling back to `mongoid (8.1.5)` fixes the issue.

        1. Mongoid-9x-issues.md
          8 kB
        2. widget.rb
          3 kB

            Assignee:
            jamis.buck@mongodb.com Jamis Buck
            Reporter:
            adam.hampton@sailpoint.com Adam Hampton
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated: