Implement a server event queue using beanstalkd

This commit is contained in:
Trever Fischer
2012-09-03 16:07:09 -04:00
parent 97da13000c
commit 4ca5e20e53
15 changed files with 139 additions and 3 deletions

60
api/events.py Normal file
View File

@@ -0,0 +1,60 @@
from django.conf import settings
from minecraft.models import Server
from json import dumps, JSONEncoder
import beanstalkc
from django.contrib.auth.models import User
class EventEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, Event):
return {'type': obj.type, 'payload': obj.data}
return super(EventEncoder, self).default(obj)
class Event(object):
def __init__(self, type, data):
self.type = type
self.data = data
class BroadcastEvent(Event):
def __init__(self, message):
super(BroadcastEvent, self).__init__(type='broadcast', data={'message':
message})
class PlayerMessageEvent(Event):
def __init__(self, user, message):
super(PlayerMessageEvent, self).__init__(type='player-message',
data={'message': message, 'player': user})
def server_queue(server, users=[]):
queueName = 'caminus-broadcast-%s'%server.id
queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
port=settings.CAMINUS_BEANSTALKD_PORT)
queue.use(queueName)
queue.watch(queueName)
if len(users) > 0:
for user in users:
queue.watch("caminus-user-%s"%user)
return queue
def send_server_event(server, event):
if settings.CAMINUS_USE_BEANSTALKD:
queue = server_queue(server)
json = dumps(event, cls=EventEncoder)
queue.put(json)
def server_broadcast(message, *args):
message = message%args
for server in Server.objects.all():
event = BroadcastEvent(message)
send_server_event(server, event)
def user_message(user, message, *args):
player = user.minecraftprofile.mc_username
player_message(player, message, *args)
def player_message(playername, message, *args):
message = message%args
for server in Server.objects.all():
event = PlayerMessageEvent(playername, message)
send_server_event(server, event)

View File

@@ -9,6 +9,7 @@ from urllib2 import urlopen
import json
from datetime import datetime
from models import cachePlayerList
from servers import server_queue, user_queue
class MOTDHandler(AnonymousBaseHandler):
allowed_methods = ('GET',)
@@ -76,6 +77,23 @@ class ServerPingHandler(BaseHandler):
def read(self, request):
return {'identity': request.server}
class ServerEventHandler(BaseHandler):
allowed_methods = ('GET', 'POST')
def read(self, request):
queue = server_queue(request.server)
queue.watch('caminus-broadcast-%s'%request.server.id)
events = []
job = queue.reserve(timeout=30)
if job:
events.append({'id': job.jid, 'event': json.loads(job.body)})
return {'events': events}
def create(self, request):
queue = server_queue(request.server)
queue.delete(int(request.POST['job']))
return {'result': 'success'}
class PollHandler(BaseHandler):
allowed_methods = ('GET',)

View File

View File

View File

@@ -5,6 +5,8 @@ from django.contrib.auth.models import User
from minecraft.models import MinecraftProfile, Server, PlayerSession, MOTD, Ban
from local.models import Quote
import hashlib
import events
from django.conf import settings
class ServerPingTest(unittest.TestCase):
def setUp(self):
@@ -25,6 +27,40 @@ class ServerPingTest(unittest.TestCase):
resp = self.client.get('/api/server/whoami', HTTP_AUTHORIZATION='X-Caminus %s'%(self.token))
self.assertEqual(resp.status_code, 200)
if settings.CAMINUS_USE_BEANSTALKD:
class ServerEventTest(unittest.TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
self.user.minecraftprofile.mc_username = "ValidUsername"
self.user.minecraftprofile.save()
self.server = Server.objects.create(hostname='localhost', secret='secret')
tokenHash = hashlib.sha1()
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
def tearDown(self):
self.user.delete()
self.server.delete()
def testBroadcast(self):
events.server_broadcast("Test message")
response = json.loads(self.client.get('/api/server/events',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertTrue(len(response['events']) > 0)
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertEqual(response['result'], 'success')
def testUserMessage(self):
events.user_message(self.user, "Test user message")
response = json.loads(self.client.get('/api/server/events',
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertTrue(len(response['events']) > 0)
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
self.assertEqual(response['result'], 'success')
class MOTDTest(unittest.TestCase):
def setUp(self):
self.client = Client()

View File

@@ -40,6 +40,7 @@ class ServerResource(Resource):
urlpatterns = patterns('api',
url(r'^motd/(?P<username>.*)$', motdHandler),
url(r'^server/whoami$', ServerResource(handlers.ServerPingHandler)),
url(r'^server/events$', ServerResource(handlers.ServerEventHandler)),
url(r'^server/economy/(?P<playername>.*)$', ServerResource(handlers.EconomyHandler)),
url(r'^server/session/(?P<playername>.*)/new$', ServerResource(handlers.NewPlayerSessionHandler)),
url(r'^server/session/(?P<playername>.*)/close$', ServerResource(handlers.ClosePlayerSessionHandler)),

View File

@@ -1,6 +1,7 @@
from django.db import models
from django.contrib.auth.models import User
import api
from caminus.api.events import user_message
from notification import models as notification
from django.core.urlresolvers import reverse
@@ -57,6 +58,7 @@ class Award(models.Model):
super(Award, self).save(*args, **kwargs)
api.badge_awarded.send_robust(sender=intern(str(self.badge.slug)), award=self)
notification.send([self.user], "badge_awarded", {"award": self, 'notice_description': self.badge, 'notice_url': reverse('user_profile')})
user_message(self.user, "You have been awarded the '%s' badge."%self.badge)
def __unicode__(self):
return "%s for %s"%(self.badge.__unicode__(), self.user.__unicode__())

View File

@@ -9,6 +9,7 @@ from django.template import RequestContext
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
from notification import models as notification
from api.servers import server_broadcast, user_broadcast
def index(request):
forums = models.Forum.objects.filter(parent=None)
@@ -44,6 +45,8 @@ def reply(request, topicID=None):
if reply.parent.user != request.user:
notification.send([reply.parent.user], "forum_reply", {"reply": reply, 'notice_url': reverse('forums.views.post', kwargs={'id':reply.id}), 'notice_description': reply.topic().title})
messages.info(request, "Reply successful")
user_message(reply.parent.user, "%s replied to your post in '%s'",
request.user, reply.topic().title)
return HttpResponseRedirect(reverse('forums.views.post', kwargs={"id":reply.id}))
return render_to_response('forums/reply.html', {"parent":parentPost, "form":form}, context_instance = RequestContext(request))
@@ -81,6 +84,7 @@ def newTopic(request, forumID=None):
topic.rootPost = reply
topic.save()
messages.info(request, "Posting successful")
server_broadcast("New forum topic: %s", topic.title)
return HttpResponseRedirect(reverse('forums.views.post', kwargs={'id': reply.id}))
return render_to_response('forums/newTopic.html', {"forum":parentForum, "replyForm":replyForm, "topicForm": topicForm}, context_instance = RequestContext(request))

View File

@@ -72,7 +72,7 @@ class InviteManageTest(TestCase):
self.user.delete()
def testCreateMaxInvites(self):
settings.CAMINUS_MAX_INVITES = 800
settings.CAMINUS_MAX_INVITES = 80
for i in range(0, settings.CAMINUS_MAX_INVITES*2):
self.client.get(reverse('local.views.createInvite'))
self.assertEqual(len(self.user.invites.all()),

View File

@@ -12,6 +12,7 @@ from django.contrib.auth import authenticate, login
from django.core.exceptions import ObjectDoesNotExist
import forms
import models
from api.events import user_message
from forums.models import Forum
from minecraft.forms import ProfileForm
from minecraft.models import MinecraftProfile
@@ -79,6 +80,7 @@ def register(request):
profile.save()
user = authenticate(username=userForm.cleaned_data['username'], password=userForm.cleaned_data['password'])
notification.send_now([invite.creator], "invite_accepted", {"new_user": user})
user_message(invite.creator, "%s has accepted your invite."%(user.username))
login(request, user)
del request.session['profile-invite']
return HttpResponseRedirect(reverse('welcome'))

View File

@@ -1,7 +1,6 @@
from django.db.models.signals import post_syncdb
import badges.api
import badges.models
from local import update_badges
from django.contrib.auth.models import User
def create_playtime_badges(app, created_models, verbosity, **kwargs):

View File

@@ -3,7 +3,6 @@ from django.contrib.auth.models import User, Group
from django.db.models.signals import post_save
from django.core.cache import cache
from django.core.exceptions import ObjectDoesNotExist
from minecraft import update_badges
import socket
import datetime
@@ -82,6 +81,7 @@ class PlayerSession(models.Model):
def save(self, *args, **kwargs):
super(PlayerSession, self).save(*args, **kwargs)
if self.end:
from minecraft import update_badges
update_badges(self.player.user)
def create_profile(sender, instance, created, **kwargs):

View File

@@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from notification import models as notification
from django.contrib import messages
from api.events import user_message
@login_required
def create(request):
@@ -22,6 +23,8 @@ def create(request):
petition.save()
adminUsers = User.objects.filter(is_staff=True)
notification.send(adminUsers, "petition_opened", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
for user in adminUsers:
user_message(user, "%s has opened a petition."%(request.user))
messages.info(request, "Petition created.")
return HttpResponseRedirect(reverse('petition.views.view', kwargs={"id":petition.id}))
return render_to_response('petition/create.html', {'form':form}, context_instance = RequestContext(request))
@@ -54,6 +57,10 @@ def comment(request, id):
comment.body = form.cleaned_data['body']
comment.petition = petition
comment.save()
adminUsers = User.objects.filter(is_staff=True)
for user in adminUsers:
user_message(user, "%s has opened a petition."%(request.user))
notification.send(adminUsers, "petition_commented", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id, 'comment': comment})
if comment.author != petition.author:
notification.send([petition.author], "petition_commented", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id, 'comment': comment})
messages.info(request, "Comment added.")
@@ -65,6 +72,9 @@ def close(request, id):
petition.closed = True
petition.save()
if petition.author != request.user:
user_notification(petition.author, "One of your petitions has been closed.")
notification.send([petition.author], "petition_closed", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
adminUsers = User.objects.filter(is_staff=True)
notification.send([adminUsers], "petition_closed", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
messages.info(request, "Petition closed.")
return HttpResponseRedirect(reverse('petition.views.view', kwargs={"id":petition.id}))

View File

@@ -8,3 +8,4 @@ south==0.7.3
django-piston==0.2.2
coverage
pydot==1.0.28
beanstalkc

View File

@@ -190,6 +190,9 @@ APPVERSION_GIT_REPO = os.path.sep.join((os.path.dirname(__file__), '.git'))
CAMINUS_MAX_INVITES = 2
CAMINUS_NEWS_FORUM_ID = 1
CAMINUS_USE_BEANSTALKD = False
CAMINUS_BEANSTALKD_HOST = 'localhost'
CAMINUS_BEANSTALKD_PORT = 11300
# Load any site-local overrides, such as camin.us' database settings, etc
try: