【问题标题】:Saving a model in a separate thread将模型保存在单独的线程中
【发布时间】:2019-08-01 16:01:41
【问题描述】:

在我的简单 web 应用程序中,我有一个名为 Document 的模型。创建文档时,它是空的。然后用户可以请求生成它,这意味着它的内容是用数据填充的。由于这个生成步骤可能需要一些时间,所以它是一个异步请求:服务器启动一个线程来生成文档,用户得到一个快速响应说生成过程开始,一段时间后生成结束,数据库更新了。

这是描述模型的代码:

import time
from threading import Thread
from django.db import models

STATE_EMPTY = 0
STATE_GENERATING = 1
STATE_READY = 2


class Document(models.Model):
    text =  models.TextField(blank=True, null=True)
    state = models.IntegerField(default=STATE_EMPTY, choices=(
        (STATE_EMPTY, 'empty'),
        (STATE_GENERATING, 'generating'),
        (STATE_READY, 'ready'),
    ))

    def generate(self):
        def generator():
            time.sleep(5)
            self.state = STATUS_READY
            self.text = 'This is the content of the document'

        self.state = STATE_GENERATING
        self.save()
        t = Thread(target=generator, name='GeneratorThread')
        t.start()

如您所见,generate 函数会更改状态、保存文档并生成线程。线程工作了一段时间(嗯,......睡了一段时间),然后改变状态和内容。

这是对应的测试:

    def test_document_can_be_generated_asynchronously(self):
        doc = Document()
        doc.save()
        self.assertEqual(STATE_EMPTY, doc.state)
        doc.generate()
        self.assertEqual(STATE_GENERATING, doc.state)

        time.sleep(8)
        self.assertEqual(STATE_READY, doc.state)
        self.assertEqual('This is the content of the document', doc.text)

此测试通过。文档对象正确地经历了所有预期的变化。

不幸的是,代码是错误的:更改文档内容后,它永远不会保存,因此更改不是持久的。这可以通过在测试中添加以下行来验证:

        self.assertEqual(STATE_READY, Document.objects.first().state)

此断言失败:

    self.assertEqual(STATE_READY, Document.objects.first().state)
AssertionError: 2 != 1

解决方案很简单:只需在generator 函数的末尾添加self.save()。但这会导致不同类型的问题:

Destroying test database for alias 'default'...
Traceback (most recent call last):
  File ".../virtualenvs/DjangoThreadTest-elBGAiyX/lib/python3.7/site-packages/django/db/backends/utils.py", line 82, in _execute
    return self.cursor.execute(sql)
psycopg2.errors.ObjectInUse: database "test_postgres" is being accessed by other users
DETAIL:  There is 1 other session using the database.


The above exception was the direct cause of the following exception:

Traceback (most recent call last):
...
  File ".../virtualenvs/DjangoThreadTest-elBGAiyX/lib/python3.7/site-packages/django/db/backends/utils.py", line 82, in _execute
    return self.cursor.execute(sql)
django.db.utils.OperationalError: database "test_postgres" is being accessed by other users
DETAIL:  There is 1 other session using the database.

问题似乎与放置在不同线程中的save() 有关。使用的引擎似乎不会影响结果:我在使用 postgresql(如图所示)和 sqlite 时获得几乎相同的错误消息(在这种情况下,错误类似于“数据库表已锁定”)。

一些类似的问题获得了诸如“只需使用 Celery 来管理繁重的处理任务”之类的回复。我宁愿了解我做错了什么以及如何使用 Django 工具来解决它。其实没有繁重的处理,也不需要扩展到大用户(webapp当时是一个用户使用)

【问题讨论】:

    标签: python django multithreading


    【解决方案1】:

    当您生成一个新线程时,Django 会为该线程创建一个到数据库的新连接。通常,所有连接都在请求周期的开始/结束和测试运行结束时关闭。但是如果线程是手动生成的,则没有关闭连接的代码 - 线程结束,其本地数据被破坏但连接没有在数据库端正确关闭(如果您感兴趣,连接存储在 thread.local 对象中)。

    因此,要解决此问题,您必须在线程结束时手动关闭连接。

    from django.db import connection
    
    def generate(self):
        def generator():
            time.sleep(5)
            self.state = STATUS_READY
            self.text = 'This is the content of the document'
            self.save()
    
            connection.close()
    
        self.state = STATE_GENERATING
        self.save()
        t = Thread(target=generator, name='GeneratorThread')
        t.start()
    

    【讨论】:

    • 感谢您的回答。不幸的是,这并不能解决问题。访问数据库没有再冲突了,但是断言还是失败,好像save()没有效果一样。
    • 问题似乎是在其他线程中对save() 的调用停止,直到测试结束。所以保存实际上是在评估断言之后发生的。如果我尝试加入该线程而不是休眠 8 秒,则测试会挂起。
    • @Spiros 我认为在请求/响应周期中,代码会很好地工作。您的问题的情况可能是由 TestCase 的事务性质引起的 - 它将测试代码包装到事务中,因此,您无法在主线程之外访问数据库中的文档(使用另一个连接)。这似乎是一个自然限制,因为如果禁用事务,则必须在每次测试后重新创建整个测试数据库。我不确定它为什么会挂起而不仅仅是中断。您可以尝试使用 SimpleTestCase 代替 TestCase 并查看它是否有效。
    猜你喜欢
    • 1970-01-01
    • 2022-01-16
    • 2011-11-03
    • 2011-07-12
    • 2022-01-04
    • 2013-08-28
    • 1970-01-01
    • 2012-11-11
    • 2021-09-18
    相关资源
    最近更新 更多