Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
b186d0e
Initial Django node backend
dcramer Sep 10, 2013
91bbaa0
Switch Node pk to UUID
dcramer Sep 15, 2013
57b6643
Basic tests and working django implementation
dcramer Sep 15, 2013
e17fa3d
remove invalid src field from repr
dcramer Sep 15, 2013
079e79a
Initial work on node field
dcramer Sep 15, 2013
56ae3fd
Add NodeStorage.create
dcramer Sep 15, 2013
a48bf5b
Swap in NodeField on Event
dcramer Sep 15, 2013
7904d85
Add set/create tests and ensure create returns node id
dcramer Sep 15, 2013
9f0480d
Automatically manage node data
dcramer Sep 15, 2013
733452d
Dont hard error outside of test env for missing data
dcramer Sep 15, 2013
e929d81
Bind node data where used
dcramer Sep 15, 2013
1d62444
Merge branch 'master' into node-store
dcramer Oct 18, 2013
ba88e24
Add multi backend to nodestore
dcramer Oct 18, 2013
2e7d1f5
Remove timestamp from settable node commands, add set_multi tests, an…
dcramer Oct 18, 2013
5360c7c
First pass at Riak nodestore backend
dcramer Oct 18, 2013
fce4938
Add Riak integration tests
dcramer Oct 19, 2013
bd4417e
Make require_riak a thing
dcramer Oct 19, 2013
8a9acbb
Add riak to test deps
dcramer Oct 19, 2013
4384f40
Fix list of nodes
dcramer Oct 19, 2013
0517ecc
Specify resolver at client level
dcramer Oct 19, 2013
d68dd05
Move pytest riak helper into testutils
dcramer Oct 19, 2013
d2c8d13
Fix call to riak check
dcramer Oct 19, 2013
1b25a92
Tests for multi backend
dcramer Oct 19, 2013
b0af57b
Add tests for node data transitions
dcramer Oct 20, 2013
eccd884
Expand test coverage for event list and json feed
dcramer Oct 20, 2013
ac59037
Ensure nodes are bound in event list
dcramer Oct 20, 2013
f85e9eb
Ensure datetime reflects accurate ordering for tests
dcramer Oct 20, 2013
5d15a3e
Support return id clauses in test case
dcramer Oct 20, 2013
7f81b00
Index Node.timestamp
dcramer Oct 20, 2013
efafaf7
Add Node to cleanup
dcramer Oct 20, 2013
99e957e
Correct node deletions (they're not bound to projects)
dcramer Oct 20, 2013
26b3fe8
Add node deletion support (automatic via group deletion)
dcramer Oct 20, 2013
f041ebc
Move node cleanup to backend
dcramer Oct 20, 2013
59a9429
Correct event_id fetching for postgres
dcramer Oct 20, 2013
13f8457
Allow read choice to be customized
dcramer Oct 25, 2013
ce2c909
Merge branch 'master' into node-store
dcramer Oct 26, 2013
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
language: python
services:
- memcached
- riak
- mysql
- postgresql
- redis-server
python:
- "2.6"
- "2.7"
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
'nydus',
'mock>=0.8.0',
'redis',
'riak',
'unittest2',
]

Expand Down
2 changes: 2 additions & 0 deletions src/sentry/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ def get_instance(path, options):

buffer = get_instance(settings.SENTRY_BUFFER, settings.SENTRY_BUFFER_OPTIONS)
quotas = get_instance(settings.SENTRY_QUOTAS, settings.SENTRY_QUOTA_OPTIONS)
nodestore = get_instance(
settings.SENTRY_NODESTORE, settings.SENTRY_NODESTORE_OPTIONS)
search = get_instance(settings.SENTRY_SEARCH, settings.SENTRY_SEARCH_OPTIONS)
8 changes: 7 additions & 1 deletion src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@
'kombu.transport.django',
'raven.contrib.django.raven_compat',
'sentry',
'sentry.nodestore',
'sentry.search',
'sentry.plugins.sentry_interface_types',
'sentry.plugins.sentry_mail',
Expand Down Expand Up @@ -508,10 +509,11 @@
# Redis connection information (see Nydus documentation)
SENTRY_REDIS_OPTIONS = {}

# Buffer backend to use
# Buffer backend
SENTRY_BUFFER = 'sentry.buffer.Buffer'
SENTRY_BUFFER_OPTIONS = {}

# Quota backend
SENTRY_QUOTAS = 'sentry.quotas.Quota'
SENTRY_QUOTA_OPTIONS = {}

Expand All @@ -521,6 +523,10 @@
# The maximum number of events per minute the system should accept.
SENTRY_SYSTEM_MAX_EVENTS_PER_MINUTE = 0

# Node storage backend
SENTRY_NODESTORE = 'sentry.nodestore.django.DjangoNodeStorage'
SENTRY_NODESTORE_OPTIONS = {}

# Search backend
SENTRY_SEARCH = 'sentry.search.django.DjangoSearchBackend'
SENTRY_SEARCH_OPTIONS = {}
Expand Down
1 change: 1 addition & 0 deletions src/sentry/db/models/fields/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@

from .bounded import * # NOQA
from .gzippeddict import * # NOQA
from .node import * # NOQA
131 changes: 131 additions & 0 deletions src/sentry/db/models/fields/node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
sentry.db.models.fields.node
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import absolute_import

import collections
import logging
import warnings

from django.db import models
from django.db.models.signals import post_delete

from sentry.utils.cache import memoize
from sentry.utils.compat import pickle
from sentry.utils.strings import decompress, compress

from .gzippeddict import GzippedDictField

__all__ = ('NodeField',)

logger = logging.getLogger('sentry.errors')


class NodeData(collections.MutableMapping):
def __init__(self, id, data=None):
self.id = id
self._node_data = data

def __getitem__(self, key):
return self.data[key]

def __setitem__(self, key, value):
self.data[key] = value

def __delitem__(self, key):
del self.data[key]

def __iter__(self):
return iter(self.data)

def __len__(self):
return len(self.data)

def __repr__(self):
cls_name = type(self).__name__
if self._node_data:
return '<%s: id=%s data=%r>' % (
cls_name, self.id, repr(self._node_data))
return '<%s: id=%s>' % (cls_name, self.id,)

@memoize
def data(self):
from sentry import app

if self._node_data is not None:
return self._node_data

elif self.id:
warnings.warn('You should populate node data before accessing it.')
return app.nodestore.get(self.id) or {}

return {}

def bind_data(self, data):
self._node_data = data


class NodeField(GzippedDictField):
"""
Similar to the gzippedictfield except that it stores a reference
to an external node.
"""
__metaclass__ = models.SubfieldBase

def contribute_to_class(self, cls, name):
super(NodeField, self).contribute_to_class(cls, name)
post_delete.connect(
self.on_delete,
sender=self.model,
weak=False)

def on_delete(self, instance, **kwargs):
from sentry import app

value = getattr(instance, self.name)
if not value.id:
return

app.nodestore.delete(value.id)

def to_python(self, value):
if isinstance(value, basestring) and value:
try:
value = pickle.loads(decompress(value))
except Exception, e:
logger.exception(e)
value = {}
elif not value:
value = {}

if 'node_id' in value:
node_id = value.pop('node_id')
data = None
else:
node_id = None
data = value

return NodeData(node_id, data)

def get_prep_value(self, value):
from sentry import app

if not value and self.null:
# save ourselves some storage
return None

# TODO(dcramer): we should probably do this more intelligently
# and manually
if not value.id:
value.id = app.nodestore.create(value.data)
else:
app.nodestore.set(value.id, value.data)

return compress(pickle.dumps({
'node_id': value.id
}))
16 changes: 16 additions & 0 deletions src/sentry/db/models/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,19 @@ def get_from_cache(self, **kwargs):

def create_or_update(self, **kwargs):
return create_or_update(self.model, **kwargs)

def bind_nodes(self, object_list, *node_names):
from sentry import app

object_node_list = []
for name in node_names:
object_node_list.extend((getattr(i, name) for i in object_list if getattr(i, name).id))

node_ids = [n.id for n in object_node_list]
if not node_ids:
return

node_results = app.nodestore.get_multi(node_ids)

for node in object_node_list:
node.bind_data(node_results.get(node.id) or {})
5 changes: 3 additions & 2 deletions src/sentry/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
)
from sentry.db.models import (
Model, GzippedDictField, BoundedIntegerField, BoundedPositiveIntegerField,
update, sane_repr
NodeField, update, sane_repr
)
from sentry.manager import (
GroupManager, ProjectManager, MetaManager, InstanceMetaManager, BaseManager,
Expand Down Expand Up @@ -482,7 +482,6 @@ class EventBase(Model):
max_length=MAX_CULPRIT_LENGTH, blank=True, null=True,
db_column='view')
checksum = models.CharField(max_length=32, db_index=True)
data = GzippedDictField(blank=True, null=True)
num_comments = BoundedPositiveIntegerField(default=0, null=True)
platform = models.CharField(max_length=64, null=True)

Expand Down Expand Up @@ -583,6 +582,7 @@ class Group(EventBase):
time_spent_count = BoundedIntegerField(default=0)
score = BoundedIntegerField(default=0)
is_public = models.NullBooleanField(default=False, null=True)
data = GzippedDictField(blank=True, null=True)

objects = GroupManager()

Expand Down Expand Up @@ -726,6 +726,7 @@ class Event(EventBase):
time_spent = BoundedIntegerField(null=True)
server_name = models.CharField(max_length=128, db_index=True, null=True)
site = models.CharField(max_length=128, db_index=True, null=True)
data = NodeField(blank=True, null=True)

objects = BaseManager()

Expand Down
9 changes: 9 additions & 0 deletions src/sentry/nodestore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
sentry.nodestore
~~~~~~~~~~~~~~~~

:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import absolute_import
67 changes: 67 additions & 0 deletions src/sentry/nodestore/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""
sentry.nodestore.base
~~~~~~~~~~~~~~~~~~~~~

:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import absolute_import

import uuid


class NodeStorage(object):
def create(self, data):
"""
>>> key = nodestore.create({'foo': 'bar'})
"""
node_id = self.generate_id()
self.set(node_id, data)
return node_id

def delete(self, id):
"""
>>> nodestore.delete('key1')
"""
raise NotImplementedError

def get(self, id):
"""
>>> data = nodestore.get('key1')
>>> print data
"""
raise NotImplementedError

def get_multi(self, id_list):
"""
>>> data_map = nodestore.get_multi(['key1', 'key2')
>>> print 'key1', data_map['key1']
>>> print 'key2', data_map['key2']
"""
return dict(
(id, self.get(id))
for id in id_list
)

def set(self, id, data):
"""
>>> nodestore.set('key1', {'foo': 'bar'})
"""
raise NotImplementedError

def set_multi(self, values):
"""
>>> nodestore.set_multi({
>>> 'key1': {'foo': 'bar'},
>>> 'key2': {'foo': 'baz'},
>>> })
"""
for id, data in values.iteritems():
self.set(id=id, data=data)

def generate_id(self):
return uuid.uuid4().hex

def cleanup(self, cutoff_timestamp):
raise NotImplementedError
9 changes: 9 additions & 0 deletions src/sentry/nodestore/django/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""
sentry.nodestore.django
~~~~~~~~~~~~~~~~~~~~~~~

:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from .backend import DjangoNodeStorage # NOQA
46 changes: 46 additions & 0 deletions src/sentry/nodestore/django/backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
sentry.nodestore.django.backend
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:copyright: (c) 2010-2013 by the Sentry Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""

from __future__ import absolute_import

from django.utils import timezone

from sentry.db.models import create_or_update
from sentry.nodestore.base import NodeStorage

from .models import Node


class DjangoNodeStorage(NodeStorage):
def delete(self, id):
Node.objects.filter(id=id).delete()

def get(self, id):
try:
return Node.objects.get(id=id).data
except Node.DoesNotExist:
return None

def get_multi(self, id_list):
return dict(
(n.id, n.data)
for n in Node.objects.filter(id__in=id_list)
)

def set(self, id, data):
create_or_update(
Node,
id=id,
defaults={
'data': data,
'timestamp': timezone.now(),
},
)

def cleanup(self, cutoff_timestamp):
Node.objects.filter(timestamp__lte=cutoff_timestamp).delete()
Loading