Implement basic varnish purging

This allows all models inherited from PgModel to specify which
URLs to purge by either setting a field or defining a function
called purge_urls, at which point they will be purged whenever
the save signal is fired.

Also implements a form under /admin/purge/ that allows for manual
purging. This should probably be extended in the future to show
the status of the pgq slaves, but that will come later.

Includes a SQL function that posts the expires to a pgq queue. For
a local deployment, this can be replaced with a simple void function
to turn off varnish purging.
This commit is contained in:
Magnus Hagander
2011-06-14 19:48:48 +02:00
parent d9e26b9518
commit f92709d2a6
19 changed files with 181 additions and 24 deletions

View File

@ -1,19 +1,22 @@
from django.db import models from django.db import models
from pgweb.util.bases import PgModel
class ContributorType(models.Model): class ContributorType(PgModel, models.Model):
typename = models.CharField(max_length=32, null=False, blank=False) typename = models.CharField(max_length=32, null=False, blank=False)
sortorder = models.IntegerField(null=False, default=100) sortorder = models.IntegerField(null=False, default=100)
extrainfo = models.TextField(null=True, blank=True) extrainfo = models.TextField(null=True, blank=True)
detailed = models.BooleanField(null=False, default=True) detailed = models.BooleanField(null=False, default=True)
purge_urls = ('community/contributors/', )
def __unicode__(self): def __unicode__(self):
return self.typename return self.typename
class Meta: class Meta:
ordering = ('sortorder',) ordering = ('sortorder',)
class Contributor(models.Model): class Contributor(PgModel, models.Model):
ctype = models.ForeignKey(ContributorType) ctype = models.ForeignKey(ContributorType)
lastname = models.CharField(max_length=100, null=False, blank=False) lastname = models.CharField(max_length=100, null=False, blank=False)
firstname = models.CharField(max_length=100, null=False, blank=False) firstname = models.CharField(max_length=100, null=False, blank=False)
@ -23,6 +26,8 @@ class Contributor(models.Model):
location = models.CharField(max_length=100, null=True, blank=True) location = models.CharField(max_length=100, null=True, blank=True)
contribution = models.TextField(null=True, blank=True) contribution = models.TextField(null=True, blank=True)
purge_urls = ('community/contributors/', )
def __unicode__(self): def __unicode__(self):
return "%s %s" % (self.firstname, self.lastname) return "%s %s" % (self.firstname, self.lastname)

View File

@ -36,6 +36,11 @@ class Version(models.Model):
class Meta: class Meta:
ordering = ('-tree', ) ordering = ('-tree', )
def purge_urls(self):
yield '/$'
yield 'versions.rss'
# FIXME: probably a lot more?
class Country(models.Model): class Country(models.Model):
name = models.CharField(max_length=100, null=False, blank=False) name = models.CharField(max_length=100, null=False, blank=False)

View File

@ -1,9 +1,9 @@
from django.shortcuts import render_to_response, get_object_or_404 from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponse, Http404 from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.template import TemplateDoesNotExist, loader, Context from django.template import TemplateDoesNotExist, loader, Context
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Count from django.db.models import Count
from django.db import connection from django.db import connection, transaction
from datetime import date, datetime from datetime import date, datetime
from os import uname from os import uname
@ -12,7 +12,7 @@ from pgweb.util.decorators import ssl_required, cache
from pgweb.util.contexts import NavContext from pgweb.util.contexts import NavContext
from pgweb.util.helpers import simple_form, PgXmlHelper from pgweb.util.helpers import simple_form, PgXmlHelper
from pgweb.util.moderation import get_all_pending_moderations from pgweb.util.moderation import get_all_pending_moderations
from pgweb.util.misc import get_client_ip, is_behind_cache from pgweb.util.misc import get_client_ip, is_behind_cache, varnish_purge
from pgweb.util.sitestruct import get_all_pages_struct from pgweb.util.sitestruct import get_all_pages_struct
# models needed for the pieces on the frontpage # models needed for the pieces on the frontpage
@ -147,3 +147,18 @@ def admin_pending(request):
return render_to_response('core/admin_pending.html', { return render_to_response('core/admin_pending.html', {
'app_list': get_all_pending_moderations(), 'app_list': get_all_pending_moderations(),
}) })
# Purge objects from varnish, for the admin pages
@login_required
def admin_purge(request):
if request.method == 'POST':
url = request.POST['url']
if url == '':
return HttpResponseRedirect('.')
varnish_purge(url)
transaction.commit_unless_managed()
return render_to_response('core/admin_purge.html', {
'purge_completed': '^%s' % url,
})
else:
return render_to_response('core/admin_purge.html')

View File

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from pgweb.util.bases import PgModel from pgweb.util.bases import PgModel
from pgweb.core.models import Version
from datetime import datetime from datetime import datetime
@ -24,6 +25,14 @@ class DocComment(PgModel, models.Model):
send_notification = True send_notification = True
def purge_urls(self):
yield '/docs/%s/interactive/%s' % (self.version, self.file)
try:
if Version.objects.get(tree=self.version).current:
yield '/docs/current/interactive/%s' % self.file
except Version.DoesNotExist:
pass
class Meta: class Meta:
ordering = ('-posted_at',) ordering = ('-posted_at',)

View File

@ -24,6 +24,13 @@ class Event(models.Model, PgModel):
send_notification = True send_notification = True
markdown_fields = ('details', ) markdown_fields = ('details', )
def purge_urls(self):
yield '/about/event/%s/' % self.pk
yield '/about/eventarchive/'
yield 'events.rss'
# FIXME: when to expire the front page?
yield '/$'
def __unicode__(self): def __unicode__(self):
return "%s: %s" % (self.startdate, self.title) return "%s: %s" % (self.startdate, self.title)

View File

@ -1,5 +1,7 @@
from django.db import models from django.db import models
from pgweb.util.bases import PgModel
choices_map = { choices_map = {
0: {'str': 'No', 'class': 'no', 'bgcolor': '#ffdddd'}, 0: {'str': 'No', 'class': 'no', 'bgcolor': '#ffdddd'},
1: {'str': 'Yes', 'class': 'yes', 'bgcolor': '#ddffdd'}, 1: {'str': 'Yes', 'class': 'yes', 'bgcolor': '#ddffdd'},
@ -8,10 +10,12 @@ choices_map = {
} }
choices = [(k, v['str']) for k,v in choices_map.items()] choices = [(k, v['str']) for k,v in choices_map.items()]
class FeatureGroup(models.Model): class FeatureGroup(PgModel, models.Model):
groupname = models.CharField(max_length=100, null=False, blank=False) groupname = models.CharField(max_length=100, null=False, blank=False)
groupsort = models.IntegerField(null=False, blank=False) groupsort = models.IntegerField(null=False, blank=False)
purge_urls = ('about/featurematrix/', )
def __unicode__(self): def __unicode__(self):
return self.groupname return self.groupname
@ -20,7 +24,7 @@ class FeatureGroup(models.Model):
# Return a list of all the columns for the matrix # Return a list of all the columns for the matrix
return [b for a,b in versions] return [b for a,b in versions]
class Feature(models.Model): class Feature(PgModel, models.Model):
group = models.ForeignKey(FeatureGroup, null=False, blank=False) group = models.ForeignKey(FeatureGroup, null=False, blank=False)
featurename = models.CharField(max_length=100, null=False, blank=False) featurename = models.CharField(max_length=100, null=False, blank=False)
featuredescription = models.TextField(null=False, blank=True) featuredescription = models.TextField(null=False, blank=True)
@ -33,6 +37,8 @@ class Feature(models.Model):
v84 = models.IntegerField(null=False, blank=False, default=0, verbose_name="8.4", choices=choices) v84 = models.IntegerField(null=False, blank=False, default=0, verbose_name="8.4", choices=choices)
v85 = models.IntegerField(null=False, blank=False, default=0, verbose_name="8.5a3", choices=choices) v85 = models.IntegerField(null=False, blank=False, default=0, verbose_name="8.5a3", choices=choices)
purge_urls = ('about/featurematrix/.*', )
def __unicode__(self): def __unicode__(self):
# To make it look good in the admin interface, just don't render it # To make it look good in the admin interface, just don't render it
return '' return ''

View File

@ -1,16 +1,20 @@
from django.db import models from django.db import models
class MailingListGroup(models.Model): from pgweb.util.bases import PgModel
class MailingListGroup(PgModel, models.Model):
groupname = models.CharField(max_length=64, null=False, blank=False) groupname = models.CharField(max_length=64, null=False, blank=False)
sortkey = models.IntegerField(null=False, default=10) sortkey = models.IntegerField(null=False, default=10)
purge_urls = ('community/lists/', )
def __unicode__(self): def __unicode__(self):
return self.groupname return self.groupname
class Meta: class Meta:
ordering = ('sortkey', ) ordering = ('sortkey', )
class MailingList(models.Model): class MailingList(PgModel, models.Model):
group = models.ForeignKey(MailingListGroup, null=False) group = models.ForeignKey(MailingListGroup, null=False)
listname = models.CharField(max_length=64, null=False, blank=False) listname = models.CharField(max_length=64, null=False, blank=False)
active = models.BooleanField(null=False, default=False) active = models.BooleanField(null=False, default=False)
@ -18,6 +22,8 @@ class MailingList(models.Model):
description = models.TextField(null=False, blank=True) description = models.TextField(null=False, blank=True)
shortdesc = models.TextField(null=False, blank=True) shortdesc = models.TextField(null=False, blank=True)
purge_urls = ('community/lists/', )
@property @property
def maybe_shortdesc(self): def maybe_shortdesc(self):
if self.shortdesc: if self.shortdesc:

View File

@ -13,6 +13,13 @@ class NewsArticle(PgModel, models.Model):
send_notification = True send_notification = True
markdown_fields = ('content',) markdown_fields = ('content',)
def purge_urls(self):
yield '/about/news/%s/' % self.pk
yield '/about/newsarchive/'
yield 'news.rss'
# FIXME: when to expire the front page?
yield '/$'
def __unicode__(self): def __unicode__(self):
return "%s: %s" % (self.date, self.title) return "%s: %s" % (self.date, self.title)

View File

@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from pgweb.core.models import Organisation from pgweb.core.models import Organisation
from pgweb.util.bases import PgModel from pgweb.util.bases import PgModel
class ProfessionalService(models.Model): class ProfessionalService(PgModel, models.Model):
submitter = models.ForeignKey(User, null=False, blank=False) submitter = models.ForeignKey(User, null=False, blank=False)
approved = models.BooleanField(null=False, blank=False, default=False) approved = models.BooleanField(null=False, blank=False, default=False)
@ -28,6 +28,7 @@ class ProfessionalService(models.Model):
provides_hosting = models.BooleanField(null=False, default=False) provides_hosting = models.BooleanField(null=False, default=False)
interfaces = models.CharField(max_length=512, null=True, blank=True) interfaces = models.CharField(max_length=512, null=True, blank=True)
purge_urls = ('support/professional_', )
send_notification = True send_notification = True

View File

@ -1,14 +1,21 @@
from django.db import models from django.db import models
from pgweb.util.bases import PgModel
from datetime import date from datetime import date
class PwnPost(models.Model): class PwnPost(PgModel, models.Model):
date = models.DateField(null=False, blank=False, default=date.today, unique=True) date = models.DateField(null=False, blank=False, default=date.today, unique=True)
intro = models.TextField(null=False, blank=False) intro = models.TextField(null=False, blank=False)
content = models.TextField(null=False, blank=False) content = models.TextField(null=False, blank=False)
markdown_fields = ('intro', 'content',) markdown_fields = ('intro', 'content',)
def purge_urls(self):
yield 'community/weeklynews/$'
yield 'community/weeklynews/pwn%s/' % self.linkdate()
yield 'weeklynews.rss'
def __unicode__(self): def __unicode__(self):
return "PostgreSQL Weekly News %s" % self.date return "PostgreSQL Weekly News %s" % self.date

View File

@ -10,6 +10,8 @@ class Quote(models.Model, PgModel):
send_notification = True send_notification = True
purge_urls = ('about/quotesarchive/', '/$', )
def __unicode__(self): def __unicode__(self):
if len(self.quote) > 75: if len(self.quote) > 75:
return "%s..." % self.quote[:75] return "%s..." % self.quote[:75]

View File

@ -2,31 +2,37 @@ from django.db import models
from core.models import Country from core.models import Country
class SponsorType(models.Model): from pgweb.util.bases import PgModel
class SponsorType(PgModel, models.Model):
typename = models.CharField(max_length=32, null=False, blank=False) typename = models.CharField(max_length=32, null=False, blank=False)
description = models.TextField(null=False, blank=False) description = models.TextField(null=False, blank=False)
sortkey = models.IntegerField(null=False, default=10) sortkey = models.IntegerField(null=False, default=10)
purge_urls = ('about/servers/', 'about/sponsors/', )
def __unicode__(self): def __unicode__(self):
return self.typename return self.typename
class Meta: class Meta:
ordering = ('sortkey', ) ordering = ('sortkey', )
class Sponsor(models.Model): class Sponsor(PgModel, models.Model):
sponsortype = models.ForeignKey(SponsorType, null=False) sponsortype = models.ForeignKey(SponsorType, null=False)
name = models.CharField(max_length=128, null=False, blank=False) name = models.CharField(max_length=128, null=False, blank=False)
url = models.URLField(null=False, blank=False) url = models.URLField(null=False, blank=False)
logoname = models.CharField(max_length=64, null=False, blank=False) logoname = models.CharField(max_length=64, null=False, blank=False)
country = models.ForeignKey(Country, null=False) country = models.ForeignKey(Country, null=False)
purge_urls = ('about/sponsors/', )
def __unicode__(self): def __unicode__(self):
return self.name return self.name
class Meta: class Meta:
ordering = ('name', ) ordering = ('name', )
class Server(models.Model): class Server(PgModel, models.Model):
name = models.CharField(max_length=32, null=False, blank=False) name = models.CharField(max_length=32, null=False, blank=False)
sponsors = models.ManyToManyField(Sponsor) sponsors = models.ManyToManyField(Sponsor)
dedicated = models.BooleanField(null=False, default=True) dedicated = models.BooleanField(null=False, default=True)
@ -35,6 +41,8 @@ class Server(models.Model):
location = models.CharField(max_length=128, null=False, blank=False) location = models.CharField(max_length=128, null=False, blank=False)
usage = models.TextField(null=False, blank=False) usage = models.TextField(null=False, blank=False)
purge_urls = ('about/servers/', )
def __unicode__(self): def __unicode__(self):
return self.name return self.name

View File

@ -1,6 +1,8 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from pgweb.util.bases import PgModel
from datetime import datetime from datetime import datetime
# internal text/value object # internal text/value object
@ -14,7 +16,7 @@ class SurveyAnswerValues(object):
self.votes = votes self.votes = votes
self.votespercent = votespercent self.votespercent = votespercent
class Survey(models.Model): class Survey(PgModel, models.Model):
question = models.CharField(max_length=100, null=False, blank=False) question = models.CharField(max_length=100, null=False, blank=False)
opt1 = models.CharField(max_length=100, null=False, blank=False) opt1 = models.CharField(max_length=100, null=False, blank=False)
opt2 = models.CharField(max_length=100, null=False, blank=False) opt2 = models.CharField(max_length=100, null=False, blank=False)
@ -27,6 +29,8 @@ class Survey(models.Model):
posted = models.DateTimeField(null=False, default=datetime.now) posted = models.DateTimeField(null=False, default=datetime.now)
current = models.BooleanField(null=False, default=False) current = models.BooleanField(null=False, default=False)
purge_urls = ('community/survey', )
def __unicode__(self): def __unicode__(self):
return self.question return self.question
@ -78,7 +82,7 @@ class Survey(models.Model):
# free to save this one. # free to save this one.
super(Survey, self).save() super(Survey, self).save()
class SurveyAnswer(models.Model): class SurveyAnswer(PgModel, models.Model):
survey = models.ForeignKey(Survey, null=False, blank=False, primary_key=True) survey = models.ForeignKey(Survey, null=False, blank=False, primary_key=True)
tot1 = models.IntegerField(null=False, default=0) tot1 = models.IntegerField(null=False, default=0)
tot2 = models.IntegerField(null=False, default=0) tot2 = models.IntegerField(null=False, default=0)
@ -89,6 +93,8 @@ class SurveyAnswer(models.Model):
tot7 = models.IntegerField(null=False, default=0) tot7 = models.IntegerField(null=False, default=0)
tot8 = models.IntegerField(null=False, default=0) tot8 = models.IntegerField(null=False, default=0)
purge_urls = ('community/survey', )
class SurveyLock(models.Model): class SurveyLock(models.Model):
ipaddr = models.IPAddressField(null=False, blank=False) ipaddr = models.IPAddressField(null=False, blank=False)
time = models.DateTimeField(null=False, default=datetime.now) time = models.DateTimeField(null=False, default=datetime.now)

View File

@ -108,6 +108,7 @@ urlpatterns = patterns('',
# Override some URLs in admin, to provide our own pages # Override some URLs in admin, to provide our own pages
(r'^admin/pending/$', 'pgweb.core.views.admin_pending'), (r'^admin/pending/$', 'pgweb.core.views.admin_pending'),
(r'^admin/purge/$', 'pgweb.core.views.admin_purge'),
# Uncomment the next line to enable the admin: # Uncomment the next line to enable the admin:
(r'^admin/(.*)', admin.site.root), (r'^admin/(.*)', admin.site.root),

View File

@ -1,17 +1,31 @@
from email.mime.text import MIMEText from email.mime.text import MIMEText
from django.db.models.signals import pre_save from django.db.models.signals import pre_save, post_save
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from util.middleware import get_current_user from util.middleware import get_current_user
from util.misc import sendmail from util.misc import sendmail, varnish_purge
class PgModel(object): class PgModel(object):
send_notification = False send_notification = False
purge_urls = ()
notify_fields = None notify_fields = None
modifying_user = None modifying_user = None
def PostSaveHandler(self):
"""
If a set of URLs are available as purge_urls, then send commands
to the cache to purge those urls.
"""
if callable(self.purge_urls):
purgelist = self.purge_urls()
else:
if not self.purge_urls: return
purgelist = self.purge_urls
map(varnish_purge, purgelist)
def PreSaveHandler(self): def PreSaveHandler(self):
"""If send_notification is set to True, send a default formatted notification mail""" """If send_notification is set to True, send a default formatted notification mail"""
@ -131,6 +145,11 @@ def my_pre_save_handler(sender, **kwargs):
if isinstance(instance, PgModel): if isinstance(instance, PgModel):
instance.PreSaveHandler() instance.PreSaveHandler()
def my_post_save_handler(sender, **kwargs):
instance = kwargs['instance']
if isinstance(instance, PgModel):
instance.PostSaveHandler()
def register_basic_signal_handlers(): def register_basic_signal_handlers():
pre_save.connect(my_pre_save_handler) pre_save.connect(my_pre_save_handler)
post_save.connect(my_post_save_handler)

View File

@ -1,5 +1,6 @@
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from email.mime.text import MIMEText from email.mime.text import MIMEText
from django.db import connection
from django.conf import settings from django.conf import settings
from pgweb.util.helpers import template_to_string from pgweb.util.helpers import template_to_string
@ -58,3 +59,14 @@ def get_client_ip(request):
return request.META['REMOTE_ADDR'] return request.META['REMOTE_ADDR']
else: else:
return request.META['REMOTE_ADDR'] return request.META['REMOTE_ADDR']
def varnish_purge(url):
"""
Purge the specified URL from Varnish. Will add initial anchor to the URL,
but no trailing one, so by default a wildcard match is done.
"""
url = '^%s' % url
connection.cursor().execute("SELECT varnish_purge(%s)", (url, ))

16
sql/varnish.sql Normal file
View File

@ -0,0 +1,16 @@
BEGIN;
--
-- Create a function to purge from varnish cache
-- By defalut this adds the object to a pgq queue,
-- but this function can be replaced with a void one
-- when running a development version.
--
CREATE OR REPLACE FUNCTION varnish_purge(url text)
RETURNS bigint
AS $$
SELECT pgq.insert_event('varnish', 'P', $1);
$$ LANGUAGE 'sql';
COMMIT;

View File

@ -11,7 +11,8 @@
{% block content %} {% block content %}
<p> <p>
View <a href="/admin/pending/">pending</a> moderation requests. View <a href="/admin/pending/">pending</a> moderation requests.<br/>
Purge contents from <a href="/admin/purge/">varnish</a>.
</p> </p>
<div id="content-main"> <div id="content-main">

View File

@ -0,0 +1,24 @@
{%extends "admin/base_site.html"%}
{%block breadcrumbs%}
<div class="breadcrumbs"><a href="/admin/">Home</a> &rsaquo; Pending</div>
{%endblock%}
{% block bodyclass %}change-list{% endblock %}
{% block coltype %}flex{% endblock %}
{%block content%}
<h1>Purge URL from Varnish</h1>
<div id="content-main">
{%if purge_completed %}
<div class="module">
Purge completed: {{purge_completed}}
</div>
{%endif%}
<form method="POST" action=".">
URL (regex): <input type="text" name="url">
<input type="submit" value="Purge" />
</form>
</div>
{%endblock%}