DO NOT use the callbacks that require persistence in Mongoid

I started using MongoDB at Gogobot a little while ago. While using it, I encountered some problems, but for the most part, things went pretty smooth.

Today, I encountered a bug that surprised me.

While it certainly should not have, I think it can surprise you as well, so I am writing it up here as a fair warning.

For Gogobot, the entire graph is built on top of MongoDB, all the things social are driven by it and for the most parts like I mentioned, we are pretty happy with it.

SO, What was the problem?

The entire graph is a mountable engine, we can decide to turn it on or to turn it off at will. It acts as a data warehouse and the workflows are being managed by the app.

For example:

When model X is created, app is notified and decides what to do with this notification, and so on and so forth.

Everything peachy so far, nothing we haven’t used hundreds of times in the past.

Here’s how it works.

We have a model called VisitedPlace, it’s a representation of a user that visited a certain place

Here’s the code

1
2
3
4
5
6
7
8
9
module GraphEngine
  class FbPlace
    include Mongoid::Document
    include Mongoid::Timestamps
    include GraphEngine::Notifications::NotifiableModel

    #… rest of code here
  end
end

As you can see, this model includes a module called NotifiableModel, here’s the important part from it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module GraphEngine
  module Notifications
    module NotifiableModel
      extend ActiveSupport::Concern

      included do
        after_create do
          send_notification("created")
        end
      end

      def send_notification(verb)
          # Notify the app here...
      end
    end
  end
end

Like I said, pretty standard stuff, nothing too fancy, but here’s where it’s getting tricky.

This model has a unique index on user_id and place_id. It’s a unique index and no two documents can exist in the same collection.

BUT… check this out:

1
2
  GraphEngine::VisitedPlace.create!(user_id: 1, place_id: 1) => true
  GraphEngine::VisitedPlace.create!(user_id: 1, place_id: 1) => true

The second query actually failed in the DB level, but the application still returned true.

Meaning, that after_create is actually being called even if the record is not really persisted.

How you can fix? / should you fix?

For Gogobot, I fixed it using safe mode on those models, I don’t mind the performance penalty, since I don’t want to trigger Sidekiq workers that will do all sorts of things twice or three times.

Should you do the same? I am not sure, you need to benchmark your app and see if you can fix it in another way.

Would love to hear back from you in comments/discussion

Developer