MongoEngine/mongoengine

“AttributeError: ‘_thread._local’ object has no attribute ‘no_dereferencing_class’ in Multithreaded Environment”

AdithyanJothir opened this issue · 11 comments

Hi,

Details :

In my project, I’m using ASGI’s sync_to_async utility. I believe this utility creates multiple threads using a thread pool executor, and each thread has its own thread-local storage. The attribute no_dereferencing_class, set by MongoEngine in context_managers.py, is causing an error: ‘_thread._local’ object has no attribute ‘no_dereferencing_class’. This issue is observed in the context_managers.py file.

thread_locals = threading.local()
thread_locals.no_dereferencing_class = {}


def no_dereferencing_active_for_class(cls):
    return cls in thread_locals.no_dereferencing_class

Tested on versions:

  • Python 3.12.1
  • MongoEngine 0.28.0
  • PyMongo 4.6.2

Argh this change was brought in the last release.
Could you provide a reproducible snippet containing minimal amount of code?

I have the same error with eventlet-0.35.2 when running mongoengine-0.28.0 and flask_track_usage-2.0.0.

Fallback to mongoengine-0.27.0 help remove the no_dereferencing_class error:

  File "/home/pyuser/app/dist-packages/flask_track_usage/summarization/__init__.py", line 53, in _caller
    method(**kwargs)
  File "/home/pyuser/app/dist-packages/flask_track_usage/summarization/mongoenginestorage.py", line 139, in sumUrl
    increment(sumUrlClasses, src, "url", ["url"])
  File "/home/pyuser/app/dist-packages/flask_track_usage/summarization/mongoenginestorage.py", line 38, in increment
    doc = class_dict[period].objects(date=times[period], **db_args).first()
  File "/home/pyuser/app/dist-packages/mongoengine/queryset/base.py", line 301, in first
    result = queryset[0]
  File "/home/pyuser/app/dist-packages/mongoengine/queryset/base.py", line 206, in __getitem__
    _auto_dereference=self._auto_dereference,
  File "/home/pyuser/app/dist-packages/mongoengine/queryset/base.py", line 1768, in _auto_dereference
    should_deref = not no_dereferencing_active_for_class(self._document)
  File "/home/pyuser/app/dist-packages/mongoengine/context_managers.py", line 28, in no_dereferencing_active_for_class
    return cls in thread_locals.no_dereferencing_class
  File "/home/pyuser/app/dist-packages/eventlet/corolocal.py", line 45, in __getattribute__
    return object.__getattribute__(self, attr)
AttributeError: 'local' object has no attribute 'no_dereferencing_class'

Thanks!

# activate eventlet
import os
import eventlet
eventlet.monkey_patch()

from flask_track_usage.storage.mongo import MongoEngineStorage
from mongoengine import connect

I am facing the same issue after updating to the latest version 0.28.0. The issue occurs only in the latest version, downgrading to version 0.27.0 seems to solve the issue.

sample code from my project

@auth_bp.route('/login', methods=['POST'])
def login():
    data = request.get_json()
    email = data.get('email')
    password = data.get('password')
    user = User.objects(email=email).first()
    if user and check_password_hash(user.password, password):
        return make_response('Logged in', 200)
    return make_response('Invalid email or password', 401)

The issue occurs where ever there is a query in the code.

Here is a simple code snippet to reproduce the said issue using asgiref sync to async with ThreadPoolExecuter

import asyncio
from concurrent.futures import ThreadPoolExecutor
from functools import partial
import mongoengine
from asgiref.sync import sync_to_async
from mongoengine import Document, StringField

mongoengine.connect(
    host="mongodb://localhost:27017/",
    db="test_db",
)


class UserModel(Document):
    meta = {
        "collection": "users",
    }
    name = StringField()


user_a = UserModel(name="JothirAdithyan")
UserModel.objects.insert(user_a)


async def get_docs():
    function = partial(UserModel.objects.first)
    user1 = await sync_to_async(func=function, thread_sensitive=False, executor=ThreadPoolExecutor())()
    print(user1.to_json())

if __name__ == "__main__":
    asyncio.run(get_docs())

Here is an example using just Flask (no async).

tl;dr: Downgrade to mongoengine 0.27.0 and it works

requirements.txt

Flask==3.0.2
mongoengine==0.28.0

app.py


app = Flask(__name__)

from flask import Flask
from mongoengine import connect
from mongoengine import Document, StringField

app = Flask(__name__)
app.config['MONGODB_SETTINGS'] = {
    'db': 'my_database',
    'host': 'mongodb://localhost:20000/my_database'
}
connect(db='my_database', host='mongodb://localhost:20000/my_database')

class User(Document):
    user_id = StringField(required=True)
    name = StringField()
    email = StringField(required=True)

@app.route('/')
def index():
    # Create a new user
    user = User(user_id='john_doe', email='john@example.com')
    user.save()
    return 'User created successfully!'

@app.route('/users')
def get_users():
    # Retrieve all users
    users = User.objects.all()
    user_list = ', '.join([f"{user.user_id}: {user.email}" for user in users])
    return f'Users: {user_list}'


if __name__ == "__main__":
    app.run(debug=True, port=6001)

Exception

Traceback (most recent call last):
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/flask/app.py", line 1488, in __call__
    return self.wsgi_app(environ, start_response)
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/flask/app.py", line 1466, in wsgi_app
    response = self.handle_exception(e)
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/flask/app.py", line 1463, in wsgi_app
    response = self.full_dispatch_request()
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/flask/app.py", line 872, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/flask/app.py", line 870, in full_dispatch_request
    rv = self.dispatch_request()
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/flask/app.py", line 855, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
  File "/tmp/temp-flask-mongo/app.py", line 32, in get_users
    user_list = ', '.join([f"{user.user_id}: {user.email}" for user in users])
  File "/tmp/temp-flask-mongo/app.py", line 32, in <listcomp>
    user_list = ', '.join([f"{user.user_id}: {user.email}" for user in users])
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/mongoengine/queryset/queryset.py", line 109, in _iter_results
    self._populate_cache()
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/mongoengine/queryset/queryset.py", line 128, in _populate_cache
    self._result_cache.append(next(self))
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/mongoengine/queryset/base.py", line 1636, in __next__
    _auto_dereference=self._auto_dereference,
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/mongoengine/queryset/base.py", line 1768, in _auto_dereference
    should_deref = not no_dereferencing_active_for_class(self._document)
  File "/tmp/temp-flask-mongo/venv/lib/python3.10/site-packages/mongoengine/context_managers.py", line 28, in no_dereferencing_active_for_class
    return cls in thread_locals.no_dereferencing_class
AttributeError: '_thread._local' object has no attribute 'no_dereferencing_class'

Fix

Downgrade mongoengine to 0.27.0

Recently I'm getting the same error

Thanks for the reproducible snippet, working on a fix right now

We're facing the same issue. Downgrading fixed it for now.

Just hit this myself. Looks like the patch is on the way

merged and 0.28.1 will be published in a few minutes