Search code examples
pythondjangocelerydjango-celery

Django celery task: dictionary changed size during iteration


In Django 1.8, I have this view for users to post in and notify the user's followers of the post using a celery function, but it generates a rather confusing error:

 dictionary changed size during iteration

Here is the view:

  @login_required
    def topic_reply(request, topic_id):
        tform = PostForm()
        topic = Topic.objects.get(pk=topic_id)
        args = {}
        posts = Post.objects.filter(topic= topic)
        posts =  Paginator(posts, DJANGO_SIMPLE_FORUM_REPLIES_PER_PAGE)

        if request.method == 'POST':
            post = PostForm(request.POST)

            if post.is_valid():
                p = post.save(commit = False)
                p.topic = topic
                p.title = post.cleaned_data['title']
                p.body = post.cleaned_data['body']
                p.creator = request.user
                p.user_ip = request.META['REMOTE_ADDR']

                if len(p.title)< 1:
                                p.title=p.body[:60]                                              
                    p.save()

                    #notify followers of the new post creation                        
                    title = 'title' #topic.title
                    link = 'bla' #topic.slug        
                    flwd = request.user
                    flwr_ids = FollowUser.objects.filter(followed=flwd).values('follower_id')
                    flwrs = User.objects.filter(id__in= flwr_ids).values('username','email') 

                    notify_new_post.delay(flwd, flwrs , title, link) #<- here the is the problem

                    return HttpResponseRedirect('/forum/topic/%s/?page=%s'  % (topic.slug, posts.num_pages))
                else:
                    return HttpResponseRedirect('/forum/topic/%s/?page=%s'  % (topic.slug, posts.num_pages))
        else:
            args.update(csrf(request))
            args['form'] = tform
            args['topic'] = topic
            return render_to_response('myforum/reply.html', args, 
                                      context_instance=RequestContext(request))

This happens even before anything being passed to the function (I see nothing happen at celery daemon)

Here is the celery function:

#@app.task
@task()
def notify_new_post(flwd, flwrs, topic, link):
    print 'post notification \n'
    subject = 'New post'
    from_email = '[email protected]'
    #to_list = [email]     

    for f in flwrs:

        to_email = f['email'].encode('ascii')
        print "[to_email]: " , [to_email]
        args = Context({
            'flwd': flwd,
            'recepient': f['username'],
            'link': link
           })  
       # if to_email !=[]:               
        plaintext = get_template('myforum/email_new_post.txt')
        htmltext = get_template('myforum/email_new_post.html')

        text_content = plaintext.render(args)
        html_content = htmltext.render(args)

        msg = EmailMultiAlternatives(subject, text_content, from_email, [to_email])
        msg.attach_alternative(html_content, "text/html")
        try:
            msg.send()
            print "[to_email]: " , [to_email]
            print 'message sent! \n'
        except Exception as e:  
            print '%s (%s)' % (e.message, type(e))

This is very odd to me because a very similar task for a 'topic' view works perfectly well. I've got really perplexed about this so appreciate your hints.

Update: here is the traceback

Environment:


Request Method: POST
Request URL: http://127.0.0.1:8000/forum/reply/52/

Django Version: 1.8.3
Python Version: 2.7.3
Installed Applications:
('django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'registration',
 'aricle',
 'photo',
 'contact',
 'captcha',
 'pure_pagination',
 'emoticons',
 'debug_toolbar',
 'django_markdown',
 'myforum',
 'userprofile',
 'userpics',
 'djcelery')
Installed Middleware:
(u'debug_toolbar.middleware.DebugToolbarMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'userprofile.middleware.ActiveUserMiddleware')
Traceback:
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/django/core/handlers/base.py" in get_response
  132.                     response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/django/contrib/auth/decorators.py" in _wrapped_view
  22.                 return view_func(request, *args, **kwargs)
File "/home/mypc/myproj/myforum/views.py" in topic_reply
  315.                 notify_new_post.delay(flwd, flwrs , title, link)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/celery/app/task.py" in delay
  453.         return self.apply_async(args, kwargs)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/celery/app/task.py" in apply_async
  555.             **dict(self._get_exec_options(), **options)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/celery/app/base.py" in send_task
  353.                 reply_to=reply_to or self.oid, **options
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/celery/app/amqp.py" in publish_task
  305.             **kwargs
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/messaging.py" in publish
  161.             compression, headers)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/messaging.py" in _prepare
  237.              body) = dumps(body, serializer=serializer)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/serialization.py" in dumps
  164.             payload = encoder(data)
File "/usr/lib/python2.7/contextlib.py" in __exit__
  35.                 self.gen.throw(type, value, traceback)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/serialization.py" in _reraise_errors
  59.         reraise(wrapper, wrapper(exc), sys.exc_info()[2])
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/serialization.py" in _reraise_errors
  55.         yield
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/serialization.py" in dumps
  164.             payload = encoder(data)
File "/home/mypc/.projenv/local/lib/python2.7/site-packages/kombu/serialization.py" in pickle_dumps
  356.         return dumper(obj, protocol=pickle_protocol)

Exception Type: EncodeError at /forum/reply/52/
Exception Value: dictionary changed size during iteration

Solution

  • This is probably caused by the fact that the value you pass for flwrs is a django.db.models.query.ValuesQuerySet. Django query sets are evaluated lazily, which I'd expect is not good for serialization. Remember that everything you send to a Celery task and return from it has to be serialized. So it is inadvisable to pass anything else than simple types or types that you know for a fact are okay with being serialized (e.g. a class you designed yourself or one which you've checked inside and out to make sure will serialize cleanly).

    The minimum fix I would suggest is passing list(flwrs) instead of flwrs. This would turn the query set into a plain list. I would also strongly suggest passing request.user.id as flwd instead of the user object itself. Passing ORM objects is a sure way to get unexpected behavior. (The Celery documentation mentions this.) Passing an id and reacquiring the object in the Celery task is the way to go.

    However, when I look at the code overall, I don't see why the database accesses are performed in the view rather than in the Celery task. So unless there's a line or variable use I've missed somewhere, I'd change your code to pass only request.user.id as flwd and then perform the database accesses in the Celery task. So the view would invoke the task like this:

    #notify followers of the new post creation                        
    title = 'title' #topic.title
    link = 'bla' #topic.slug        
    notify_new_post.delay(request.user.id, title, link)
    

    And the task would start like this:

    from django.contrib.auth import get_user_model
    @task()
    def notify_new_post(flwd_id, topic, link):
        user_model = get_user_model()
        flwd = user_model.objects.get(id=flwd_id)
        flwr_ids = FollowUser.objects.filter(followed=flwd).values('follower_id')
        flwrs = user_model.objects.filter(id__in= flwr_ids).values('username','email') 
    

    (Note regarding the last line: I've assumed that User is the user model that is used by your Django project so I'm using the return value of get_user_model() (assigned to user_model) rather than use User directly. If my assumption is incorrect and User is something else, then you'd have to use User as you originally did.)