Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error "undefined method 'first' for nil:NilClass (NoMethodError)" probaby caused by concurecy #257

Closed
Inversion-des opened this issue Mar 19, 2023 · 3 comments

Comments

@Inversion-des
Copy link

In lib/moneta/adapters/sqlite.rb:57 there is a block

  # (see Proxy#key?)
  def key?(key, options = {})
    @exists.execute!(key).first.first.to_i == 1
  end

and several times I got this strange error: undefined method 'first' for nil:NilClass (NoMethodError)

@exists.execute!(key).first.first.to_i == 1
                            ^^^^^^

In my case, simple retry after a small delay worked, but there may be a better solution.
I think it is somehow caused by doing this operation from parallel threads.

@asppsa
Copy link
Collaborator

asppsa commented Apr 10, 2023

Hey,

I'll have a look at that first.first code, as there might be some better way to write this that is more reliable.

In the backend feature matrix we have the Sqlite adapter listed as having unknown thread safety. From looking back through the code history, the reason for this is unclear, but it looks like this is because either the sqlite3 database itself was not good at concurrency at the time (which might be outdated), or because of issues with the ruby gem. There's some discussion of GVL/threading issues here: sparklemotion/sqlite3-ruby#287 (comment).

That discussion also points to https://www.sqlite.org/threadsafe.html which explains that the sqlite C library may be compiled in various ways wrt threadsafety, so you might want to double-check that SQLite3.threadsafe == 1 on your system.

The two alternative approaches suggested in that thread (separate connections for separate threads, or using a mutex) are both doable in Moneta. The Pool proxy can be used to maintain a connection pool with checkout/checkin handled for you per-thread; and Lock will lock the entire store so that only one thread at a time accesses it. Lock can also be enabled from Moneta.new by passing threadsafe: true.

You might want to try these two approaches to see if they help at all:

store_with_pool = Moneta.build do
  # if you're switching from `Moneta.new` to `Moneta.build`, add this for compatibility 
  use :Transformer, key: :marshal, value: :marshal

  # there are a bunch of options you can use with `:Pool` to control how many connections there are
  use :Pool max: 4 do
    adapter :Sqlite, file: 'my.db'
  end
end

store_with_mutex = Moneta.new(:Sqlite, file: 'my.db', threadsafe: true)
# equivalent to:
store_with_mutex = Moneta.build do
  use :Transformer, key: :marshal, value: :marshal
  use :Lock
  adapter :Sqlite, file: 'my.db'
end

@Inversion-des
Copy link
Author

Thank you for the detailed response!
Already checked that SQLite3.threadsafe returns 1 (called right after the Moneta.new).
So I will try to use threadsafe: true (my mistake that I missed it). And only if there will be a problem again — I will add a comment here. Until that, you can close this issue.

@Inversion-des
Copy link
Author

After one month of testing, I can say that threadsafe: true resolved the issue. It did not happen a single time.
Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants