Skip to content

Commit

Permalink
Merge pull request #34 from juntossomosmais/fix/settings
Browse files Browse the repository at this point in the history
fix(settings): change settings import mechanism
  • Loading branch information
MatheusGeiger authored Jul 19, 2023
2 parents 3d17750 + ea6444f commit 34d965b
Show file tree
Hide file tree
Showing 13 changed files with 77 additions and 180 deletions.
2 changes: 1 addition & 1 deletion django_outbox_pattern/bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from stomp.exception import StompException

from django_outbox_pattern.settings import settings
from django_outbox_pattern import settings

logger = logging.getLogger("django_outbox_pattern")

Expand Down
2 changes: 1 addition & 1 deletion django_outbox_pattern/consumers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from django.utils import timezone
from stomp.utils import get_uuid

from django_outbox_pattern import settings
from django_outbox_pattern.bases import Base
from django_outbox_pattern.payloads import Payload
from django_outbox_pattern.settings import settings

logger = logging.getLogger("django_outbox_pattern")

Expand Down
2 changes: 1 addition & 1 deletion django_outbox_pattern/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django_outbox_pattern import settings
from django_outbox_pattern.consumers import Consumer
from django_outbox_pattern.producers import Producer
from django_outbox_pattern.settings import settings

USERNAME = settings.DEFAULT_STOMP_USERNAME
PASSCODE = settings.DEFAULT_STOMP_PASSCODE
Expand Down
2 changes: 1 addition & 1 deletion django_outbox_pattern/management/commands/publish.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
from django.db import DatabaseError
from django.utils import timezone

from django_outbox_pattern import settings
from django_outbox_pattern.choices import StatusChoice
from django_outbox_pattern.exceptions import ExceededSendAttemptsException
from django_outbox_pattern.factories import factory_producer
from django_outbox_pattern.settings import settings

logger = logging.getLogger("django_outbox_pattern")

Expand Down
11 changes: 5 additions & 6 deletions django_outbox_pattern/producers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@
from stomp.exception import StompException
from stomp.utils import get_uuid

from django_outbox_pattern import settings
from django_outbox_pattern.bases import Base
from django_outbox_pattern.exceptions import ExceededSendAttemptsException
from django_outbox_pattern.settings import settings

logger = logging.getLogger("django_outbox_pattern")


class Producer(Base):
settings = settings
listener_class = settings.DEFAULT_PRODUCER_LISTENER_CLASS
published_class = settings.DEFAULT_PUBLISHED_CLASS

Expand All @@ -44,7 +43,7 @@ def stop(self):
logger.info("Producer not started")

def send(self, message, **kwargs):
generate_headers = self.settings.DEFAULT_GENERATE_HEADERS
generate_headers = settings.DEFAULT_GENERATE_HEADERS
headers = generate_headers(message)
kwargs = {
"body": json.dumps(message.body, cls=DjangoJSONEncoder),
Expand All @@ -71,15 +70,15 @@ def _send_with_retry(self, **kwargs):

attempts = 0

while attempts < self.settings.DEFAULT_MAXIMUM_RETRY_ATTEMPTS:
while attempts < settings.DEFAULT_MAXIMUM_RETRY_ATTEMPTS:
try:
self.connection.send(**kwargs)
except StompException:
attempts += 1
if attempts == 3:
sleep(self.settings.DEFAULT_PAUSE_FOR_RETRY)
sleep(settings.DEFAULT_PAUSE_FOR_RETRY)
elif attempts > 3:
sleep(self.settings.DEFAULT_WAIT_RETRY)
sleep(settings.DEFAULT_WAIT_RETRY)
else:
break
else:
Expand Down
181 changes: 47 additions & 134 deletions django_outbox_pattern/settings.py
Original file line number Diff line number Diff line change
@@ -1,137 +1,50 @@
"""
Settings for DJANGO OUTBOX are all namespaced in the DJANGO_OUTBOX_PATTERN setting.
For example your project's `settings.py` file might look like this:
DJANGO_OUTBOX_PATTERN = {
'DEFAULT_GENERATE_HEADERS': 'django_outbox_pattern.headers.handle_headers'
}
Thanks Django Rest Framework: https://github.com/encode/django-rest-framework/blob/master/rest_framework/settings.py
"""

from django.conf import settings as django_settings
from django.test.signals import setting_changed
from django.utils.module_loading import import_string

DEFAULTS = {
"DEFAULT_CONNECTION_CLASS": "stomp.StompConnection12",
"DEFAULT_CONSUMER_LISTENER_CLASS": "django_outbox_pattern.listeners.ConsumerListener",
"DEFAULT_GENERATE_HEADERS": "django_outbox_pattern.headers.generate_headers",
"DEFAULT_MAXIMUM_BACKOFF": 3600,
"DEFAULT_MAXIMUM_RETRY_ATTEMPTS": 50,
"DEFAULT_PAUSE_FOR_RETRY": 240,
"DEFAULT_WAIT_RETRY": 60,
"DEFAULT_PRODUCER_LISTENER_CLASS": "django_outbox_pattern.listeners.ProducerListener",
"DEFAULT_PUBLISHED_CLASS": "django_outbox_pattern.models.Published",
"DEFAULT_RECEIVED_CLASS": "django_outbox_pattern.models.Received",
"DEFAULT_STOMP_HOST_AND_PORTS": [("127.0.0.1", 61613)],
"DEFAULT_STOMP_QUEUE_HEADERS": {"durable": "true", "auto-delete": "false", "prefetch-count": "1"},
"DEFAULT_STOMP_HEARTBEATS": (10000, 10000),
"DEFAULT_STOMP_VHOST": "/",
"DEFAULT_STOMP_USERNAME": "guest",
"DEFAULT_STOMP_PASSCODE": "guest",
"DEFAULT_STOMP_USE_SSL": False,
"DEFAULT_STOMP_KEY_FILE": None,
"DEFAULT_STOMP_CERT_FILE": None,
"DEFAULT_STOMP_CA_CERTS": None,
"DEFAULT_STOMP_CERT_VALIDATOR": None,
"DEFAULT_STOMP_SSL_VERSION": None,
"DEFAULT_STOMP_SSL_PASSWORD": None,
"DAYS_TO_KEEP_DATA": 30,
"REMOVE_DATA_CACHE_TTL": 86400,
"OUTBOX_PATTERN_PUBLISHER_CACHE_KEY": "remove_old_messages_django_outbox_pattern_publisher",
"OUTBOX_PATTERN_CONSUMER_CACHE_KEY": "remove_old_messages_django_outbox_pattern_consumer",
}

# List of settings that may be in string import notation.
IMPORT_STRINGS = [
"DEFAULT_CONNECTION_CLASS",
"DEFAULT_CONSUMER_LISTENER_CLASS",
"DEFAULT_GENERATE_HEADERS",
"DEFAULT_PRODUCER_LISTENER_CLASS",
"DEFAULT_PUBLISHED_CLASS",
"DEFAULT_RECEIVED_CLASS",
"DEFAULT_STOMP_CERT_VALIDATOR",
]


def perform_import(val, setting_name):
"""
If the given setting is a string import notation,
then perform the necessary import or imports.
"""

if isinstance(val, str):
val = import_from_string(val, setting_name)
elif isinstance(val, (list, tuple)):
val = [import_from_string(item, setting_name) for item in val]
return val


def import_from_string(val, setting_name):
"""
Attempt to import a class from a string representation.
"""
try:
return import_string(val)
except ImportError as exc:
msg = f"Could not import '{val}' for setting '{setting_name}'. {exc.__class__}: {exc}."
raise ImportError(msg) from exc


class Setting:
"""
A settings object that allows DJANGO OUTBOX PATTERN settings to be accessed as
"""

def __init__(self, user_settings=None, defaults=None, import_strings=None):
if user_settings:
self._user_settings = user_settings
self.defaults = defaults or DEFAULTS
self.import_strings = import_strings or IMPORT_STRINGS
self._cached_attrs = set()

@property
def user_settings(self):
if not hasattr(self, "_user_settings"):
self._user_settings = getattr(django_settings, "DJANGO_OUTBOX_PATTERN", {})
return self._user_settings

def __getattr__(self, attr):
if attr not in self.defaults:
raise AttributeError(f"Invalid setting: '{attr}'")

try:
# Check if present in user settings
val = self.user_settings[attr]
except KeyError:
# Fall back to defaults
val = self.defaults[attr]

# Coerce import strings into classes
if attr in self.import_strings:
val = perform_import(val, attr)

# Cache the result
self._cached_attrs.add(attr)
setattr(self, attr, val)
return val

def reload(self):
for attr in self._cached_attrs:
delattr(self, attr)
self._cached_attrs.clear()
if hasattr(self, "_user_settings"):
delattr(self, "_user_settings")


settings = Setting(None, DEFAULTS, IMPORT_STRINGS)


def reload_settings(*args, **kwargs): # pylint:: disable=unused-argument
setting = kwargs["setting"]
if setting == "DJANGO_OUTBOX_PATTERN":
settings.reload()


setting_changed.connect(reload_settings)
DJANGO_OUTBOX_PATTERN = getattr(django_settings, "DJANGO_OUTBOX_PATTERN", {})

DEFAULT_CONNECTION_CLASS = import_string(
DJANGO_OUTBOX_PATTERN.get("DEFAULT_CONNECTION_CLASS", "stomp.StompConnection12")
)
DEFAULT_CONSUMER_LISTENER_CLASS = import_string(
DJANGO_OUTBOX_PATTERN.get("DEFAULT_CONSUMER_LISTENER_CLASS", "django_outbox_pattern.listeners.ConsumerListener")
)
DEFAULT_GENERATE_HEADERS = import_string(
DJANGO_OUTBOX_PATTERN.get("DEFAULT_GENERATE_HEADERS", "django_outbox_pattern.headers.generate_headers")
)
DEFAULT_MAXIMUM_BACKOFF = DJANGO_OUTBOX_PATTERN.get("DEFAULT_MAXIMUM_BACKOFF", 3600)
DEFAULT_MAXIMUM_RETRY_ATTEMPTS = DJANGO_OUTBOX_PATTERN.get("DEFAULT_MAXIMUM_RETRY_ATTEMPTS", 50)
DEFAULT_PAUSE_FOR_RETRY = DJANGO_OUTBOX_PATTERN.get("DEFAULT_PAUSE_FOR_RETRY", 240)
DEFAULT_WAIT_RETRY = DJANGO_OUTBOX_PATTERN.get("DEFAULT_WAIT_RETRY", 60)
DEFAULT_PRODUCER_LISTENER_CLASS = import_string(
DJANGO_OUTBOX_PATTERN.get("DEFAULT_PRODUCER_LISTENER_CLASS", "django_outbox_pattern.listeners.ProducerListener")
)
DEFAULT_PUBLISHED_CLASS = import_string(
DJANGO_OUTBOX_PATTERN.get("DEFAULT_PUBLISHED_CLASS", "django_outbox_pattern.models.Published")
)
DEFAULT_RECEIVED_CLASS = import_string(
DJANGO_OUTBOX_PATTERN.get("DEFAULT_RECEIVED_CLASS", "django_outbox_pattern.models.Received")
)
DEFAULT_STOMP_HOST_AND_PORTS = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_HOST_AND_PORTS", [("127.0.0.1", 61613)])
DEFAULT_STOMP_QUEUE_HEADERS = DJANGO_OUTBOX_PATTERN.get(
"DEFAULT_STOMP_QUEUE_HEADERS", {"durable": "true", "auto-delete": "false", "prefetch-count": "1"}
)
DEFAULT_STOMP_HEARTBEATS = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_HEARTBEATS", (10000, 10000))
DEFAULT_STOMP_VHOST = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_VHOST", "/")
DEFAULT_STOMP_USERNAME = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_USERNAME", "guest")
DEFAULT_STOMP_PASSCODE = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_PASSCODE", "guest")
DEFAULT_STOMP_USE_SSL = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_USE_SSL", False)
DEFAULT_STOMP_KEY_FILE = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_KEY_FILE", None)
DEFAULT_STOMP_CERT_FILE = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_CERT_FILE", None)
DEFAULT_STOMP_CA_CERTS = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_CA_CERTS", None)
DEFAULT_STOMP_CERT_VALIDATOR = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_CERT_VALIDATOR", None)
DEFAULT_STOMP_SSL_VERSION = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_SSL_VERSION", None)
DEFAULT_STOMP_SSL_PASSWORD = DJANGO_OUTBOX_PATTERN.get("DEFAULT_STOMP_SSL_PASSWORD", None)
DAYS_TO_KEEP_DATA = DJANGO_OUTBOX_PATTERN.get("DAYS_TO_KEEP_DATA", 30)
REMOVE_DATA_CACHE_TTL = DJANGO_OUTBOX_PATTERN.get("REMOVE_DATA_CACHE_TTL", 86400)
OUTBOX_PATTERN_PUBLISHER_CACHE_KEY = DJANGO_OUTBOX_PATTERN.get(
"OUTBOX_PATTERN_PUBLISHER_CACHE_KEY", "remove_old_messages_django_outbox_pattern_publisher"
)
OUTBOX_PATTERN_CONSUMER_CACHE_KEY = DJANGO_OUTBOX_PATTERN.get(
"OUTBOX_PATTERN_CONSUMER_CACHE_KEY", "remove_old_messages_django_outbox_pattern_consumer"
)
11 changes: 11 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@ services:
condition: service_healthy
db:
condition: service_healthy
networks:
- djangooutboxpattern

rabbitmq:
image : rabbitmq:3.8-management
volumes:
- ./tests/resources/rabbitmq:/etc/rabbitmq/
healthcheck:
test: rabbitmq-diagnostics -q ping
networks:
- djangooutboxpattern

develop:
build: .
Expand All @@ -31,6 +35,8 @@ services:
condition: service_healthy
db:
condition: service_healthy
networks:
- djangooutboxpattern

db:
image: postgres:12-alpine
Expand All @@ -43,3 +49,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
networks:
- djangooutboxpattern

networks:
djangooutboxpattern:
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "django-outbox-pattern"
version = "0.13.0"
version = "0.13.1"
description = "A django application to make it easier to use the transactional outbox pattern"
license = "MIT"
authors = ["Hugo Brilhante <[email protected]>"]
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from django.test import TransactionTestCase
from stomp.listener import TestListener

from django_outbox_pattern import settings
from django_outbox_pattern.factories import factory_consumer
from django_outbox_pattern.models import Received
from django_outbox_pattern.settings import settings


def get_callback(raise_except=False):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from django.test import TestCase
from stomp.listener import TestListener

from django_outbox_pattern import settings
from django_outbox_pattern.factories import factory_producer
from django_outbox_pattern.models import Published
from django_outbox_pattern.settings import settings


class ProducerTest(TestCase):
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/test_publish_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from django.core.management import call_command
from django.db import DatabaseError
from django.test import TestCase
from django.test import override_settings

from django_outbox_pattern import settings
from django_outbox_pattern.exceptions import ExceededSendAttemptsException
from django_outbox_pattern.management.commands.publish import Command
from django_outbox_pattern.models import Published
Expand All @@ -32,8 +32,8 @@ def test_command_on_database_error(self):
call_command("publish", stdout=self.out)
self.assertIn("Starting publisher", self.out.getvalue())

@override_settings(DJANGO_OUTBOX_PATTERN={"DEFAULT_RETRY_SEND_ATTEMPTS": 1})
def test_command_on_exceeded_send_attempts(self):
settings.DEFAULT_MAXIMUM_RETRY_ATTEMPTS = 1
with patch.object(Command.producer, "send", side_effect=ExceededSendAttemptsException(1)):
Published.objects.create(destination="test", body={})
call_command("publish", stdout=self.out)
Expand Down
18 changes: 5 additions & 13 deletions tests/unit/test_prodeucer.py → tests/unit/test_producer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from unittest.mock import patch

from django.test import TestCase
from django.test import override_settings
from stomp.exception import StompException

from django_outbox_pattern import settings
from django_outbox_pattern.exceptions import ExceededSendAttemptsException
from django_outbox_pattern.factories import factory_producer
from django_outbox_pattern.models import Published
Expand Down Expand Up @@ -34,27 +34,19 @@ def test_producer_send_event_with_context_manager(self):
producer.send_event(destination="destination", body={"message": "Test send event"})
self.assertEqual(producer.connection.send.call_count, 1)

@override_settings(
DJANGO_OUTBOX_PATTERN={
"DEFAULT_WAIT_RETRY": 1,
"DEFAULT_PAUSE_FOR_RETRY": 1,
"DEFAULT_MAXIMUM_RETRY_ATTEMPTS": 5,
}
)
def test_producer_on_exceeded_send_attempts(self):
settings.DEFAULT_WAIT_RETRY = 1
settings.DEFAULT_PAUSE_FOR_RETRY = 1
settings.DEFAULT_MAXIMUM_RETRY_ATTEMPTS = 5
with patch.object(self.producer.connection, "send", side_effect=StompException()):
published = Published.objects.create(destination="destination", body={"message": "Message test"})
self.producer.start()
with self.assertRaises(ExceededSendAttemptsException):
self.producer.send(published)
self.assertEqual(self.producer.connection.send.call_count, 5)

@override_settings(
DJANGO_OUTBOX_PATTERN={
"DEFAULT_MAXIMUM_RETRY_ATTEMPTS": 1,
}
)
def test_producer_successful_after_fail(self):
settings.DEFAULT_MAXIMUM_RETRY_ATTEMPTS = 1
with patch.object(self.producer.connection, "send", side_effect=StompException()):
published = Published.objects.create(destination="destination", body={"message": "Message test 1"})
self.producer.start()
Expand Down
Loading

0 comments on commit 34d965b

Please sign in to comment.