GithubHelp home page GithubHelp logo

cloudblue / django-cqrs Goto Github PK

View Code? Open in Web Editor NEW
110.0 7.0 26.0 872 KB

django-cqrs is an Django application, that implements CQRS data synchronization between several Django micro-services

License: Apache License 2.0

Python 98.97% Makefile 0.36% Shell 0.54% Dockerfile 0.13%
django django-cqrs sqrs microservices rabbitmq python highload

django-cqrs's Introduction

Django CQRS

pyversions PyPI Docs Coverage GitHub Workflow Status PyPI status Quality Gate Status PyPI Downloads GitHub

django-cqrs is an Django application, that implements CQRS data synchronisation between several Django microservices.

CQRS

In Connect we have a rather complex Domain Model. There are many microservices, that are decomposed by subdomain and which follow database-per-service pattern. These microservices have rich and consistent APIs. They are deployed in cloud k8s cluster and scale automatically under load. Many of these services aggregate data from other ones and usually API Composition is totally enough. But, some services are working too slowly with API JOINS, so another pattern needs to be applied.

The pattern, that solves this issue is called CQRS - Command Query Responsibility Segregation. Core idea behind this pattern is that view databases (replicas) are defined for efficient querying and DB joins. Applications keep their replicas up to data by subscribing to Domain events published by the service that owns the data. Data is eventually consistent and that's okay for non-critical business transactions.

Documentation

Full documentation is available at https://django-cqrs.readthedocs.org.

Examples

You can find an example project here

Integration

  • Setup RabbitMQ
  • Install django-cqrs
  • Apply changes to master service, according to RabbitMQ settings
# models.py

from django.db import models
from dj_cqrs.mixins import MasterMixin, RawMasterMixin


class Account(MasterMixin, models.Model):
    CQRS_ID = 'account'
    CQRS_PRODUCE = True  # set this to False to prevent sending instances to Transport
    
    
class Author(MasterMixin, models.Model):
    CQRS_ID = 'author'
    CQRS_SERIALIZER = 'app.api.AuthorSerializer'


# For cases of Diamond Multi-inheritance or in case of Proxy Django-models the following approach could be used:
from mptt.models import MPTTModel
from dj_cqrs.metas import MasterMeta

class ComplexInheritanceModel(MPTTModel, RawMasterMixin):
    CQRS_ID = 'diamond'

class BaseModel(RawMasterMixin):
    CQRS_ID = 'base'

class ProxyModel(BaseModel):
    class Meta:
        proxy = True

MasterMeta.register(ComplexInheritanceModel)
MasterMeta.register(BaseModel)
# settings.py

CQRS = {
    'transport': 'dj_cqrs.transport.rabbit_mq.RabbitMQTransport',
    'host': RABBITMQ_HOST,
    'port': RABBITMQ_PORT,
    'user': RABBITMQ_USERNAME,
    'password': RABBITMQ_PASSWORD,
}
  • Apply changes to replica service, according to RabbitMQ settings
from django.db import models
from dj_cqrs.mixins import ReplicaMixin


class AccountRef(ReplicaMixin, models.Model):
    CQRS_ID = 'account'
    
    id = models.IntegerField(primary_key=True)
    

class AuthorRef(ReplicaMixin, models.Model):
    CQRS_ID = 'author'
    CQRS_CUSTOM_SERIALIZATION = True
    
    @classmethod
    def cqrs_create(cls, sync, mapped_data, previous_data=None, meta=None):
        # Override here
        pass
        
    def cqrs_update(self, sync, mapped_data, previous_data=None, meta=None):
        # Override here
        pass
# settings.py

CQRS = {
    'transport': 'dj_cqrs.transport.RabbitMQTransport',
    'queue': 'account_replica',
    'host': RABBITMQ_HOST,
    'port': RABBITMQ_PORT,
    'user': RABBITMQ_USERNAME,
    'password': RABBITMQ_PASSWORD,
}
  • Apply migrations on both services
  • Run consumer worker on replica service. Management command: python manage.py cqrs_consume -w 2

Notes

  • When there are master models with related entities in CQRS_SERIALIZER, it's important to have operations within atomic transactions. CQRS sync will happen on transaction commit.
  • Please, avoid saving different instances of the same entity within transaction to reduce syncing and potential racing on replica side.
  • Updating of related model won't trigger CQRS automatic synchronization for master model. This needs to be done manually.
  • By default update_fields doesn't trigger CQRS logic, but it can be overridden for the whole application in settings:
settings.CQRS = {
    ...
    'master': {
        'CQRS_AUTO_UPDATE_FIELDS': True,
    },
    ...
}

or a special flag can be used in each place, where it's required to trigger CQRS flow:

instance.save(update_fields=['name'], update_cqrs_fields=True)
  • When only needed instances need to be synchronized, there is a method is_sync_instance to set filtering rule. It's important to understand, that CQRS counting works even without syncing and rule is applied every time model is updated.

Example:

class FilteredSimplestModel(MasterMixin, models.Model):
    CQRS_ID = 'filter'

    name = models.CharField(max_length=200)

    def is_sync_instance(self):
        return len(str(self.name)) > 2

Django Admin

Add action to synchronize master items from Django Admin page.

from django.db import models
from django.contrib import admin

from dj_cqrs.admin_mixins import CQRSAdminMasterSyncMixin


class AccountAdmin(CQRSAdminMasterSyncMixin, admin.ModelAdmin):
    ...


admin.site.register(models.Account, AccountAdmin)
  • If necessary, override _cqrs_sync_queryset from CQRSAdminMasterSyncMixin to adjust the QuerySet and use it for synchronization.

Utilities

Bulk synchronizer without transport (usage example: it may be used for initial configuration). May be used at planned downtime.

  • On master service: python manage.py cqrs_bulk_dump --cqrs-id=author -> author.dump
  • On replica service: python manage.py cqrs_bulk_load -i=author.dump

Filter synchronizer over transport (usage example: sync some specific records to a given replica). Can be used dynamically.

  • To sync all replicas: python manage.py cqrs_sync --cqrs-id=author -f={"id__in": [1, 2]}
  • To sync all instances only with one replica: python manage.py cqrs_sync --cqrs-id=author -f={} -q=replica

Set of diff synchronization tools:

  • To get diff and synchronize master service with replica service in K8S:
kubectl exec -i MASTER_CONTAINER -- python manage.py cqrs_diff_master --cqrs-id=author | 
    kubectl exec -i REPLICA_CONTAINER -- python manage.py cqrs_diff_replica |
    kubectl exec -i MASTER_CONTAINER -- python manage.py cqrs_diff_sync
  • If it's important to check sync and clean up deleted objects within replica service in K8S:
kubectl exec -i REPLICA_CONTAINER -- python manage.py cqrs_deleted_diff_replica --cqrs-id=author | 
    kubectl exec -i MASTER_CONTAINER -- python manage.py cqrs_deleted_diff_master |
    kubectl exec -i REPLICA_CONTAINER -- python manage.py cqrs_deleted_sync_replica

Development

  1. Python >= 3.8
  2. Install dependencies requirements/dev.txt
  3. We use isort library to order and format our imports, and black - to format the code. We check it using flake8-isort and flake8-black libraries (automatically on flake8 run).
    For convenience you may run isort . && black . to format the code.

Testing

Unit testing

  1. Python >= 3.8
  2. Install dependencies requirements/test.txt
  3. export PYTHONPATH=/your/path/to/django-cqrs/

Run tests with various RDBMS:

  • cd integration_tests
  • DB=postgres docker-compose -f docker-compose.yml -f rdbms.yml run app_test
  • DB=mysql docker-compose -f docker-compose.yml -f rdbms.yml run app_test

Check code style: flake8 Run tests: pytest

Tests reports are generated in tests/reports.

  • out.xml - JUnit test results
  • coverage.xml - Coverage xml results

To generate HTML coverage reports use: --cov-report html:tests/reports/cov_html

Integrational testing

  1. docker-compose
  2. cd integration_tests
  3. docker-compose run master

django-cqrs's People

Contributors

atikhono avatar bdjilka avatar d3rky avatar dependabot[bot] avatar ffaraone avatar gab832 avatar hairash avatar jazz-jack avatar lmasikl avatar marcserrat avatar maxipavlovic avatar net-free avatar qarlosh avatar r-s11v avatar vgrebenschikov avatar zzzevaka avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-cqrs's Issues

Problem with CQRS_ID

I've been working with Django CQRS lately and today I had a problem with it. Following the Getting started section from the docs all was working nice but when I tried to add an entity either from the django admin or with the sync command, it throws this error for each entity:

('CQRS is failed: pk = 7 (student), correlation_id = None, retries = 0.',)
No model with such CQRS_ID: student.
Model for cqrs_id student is not found.

Reading the example and the docs didn't help me. Thanks in advance

Replication in the same DB

It's possible have one project (one DB) with two domains (2 app) and replicate one model to another domain, in this another domain maybe the structure is not the same as the first one.
Thanks

Does this library support Amazon SQS ?

Instead of using RabbitMQ, does this library support Amazon SQS? django-cqrs has also the Kombu transport and Kombu supports SQS. Could you please to setup instructions for SQS ?

Dead letters commands customer settings

#39 (comment)

Hey,

I've found that this Pull Request is breaking cqrs_dead_letters retry command.

_get_consumer_settings used to return 2 arguments and with this change it start to return tuple of 3, which is breaking init_broker method in above mentioned command. Exact line:

queue_name, dead_letter_queue_name = RabbitMQTransportService.get_consumer_settings()

is raising ValueError: too many values to unpack (expected 2)

Some strange 'utils' command appears in the --help

if you try to get --help for django commands with installed django-cqrs the strange 'utils' command is in the list

python manage.py --help
...
[dj_cqrs]
    cqrs_bulk_dump
    cqrs_bulk_load
    cqrs_consume
    cqrs_dead_letters
    cqrs_deleted_diff_master
    cqrs_deleted_diff_replica
    cqrs_deleted_sync_replica
    cqrs_diff_master
    cqrs_diff_replica
    cqrs_diff_sync
    cqrs_sync
    utils
...

But if you try to run it the following error appears:

python manage.py utils --help
Traceback (most recent call last):
  File "manage.py", line 21, in <module>
    main()
  File "manage.py", line 17, in main
    execute_from_command_line(sys.argv)
  File "/usr/local/lib/python3.8/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/usr/local/lib/python3.8/site-packages/django/core/management/__init__.py", line 413, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/usr/local/lib/python3.8/site-packages/django/core/management/__init__.py", line 257, in fetch_command
    klass = load_command_class(app_name, subcommand)
  File "/usr/local/lib/python3.8/site-packages/django/core/management/__init__.py", line 40, in load_command_class
    return module.Command()
AttributeError: module 'dj_cqrs.management.commands.utils' has no attribute 'Command'

Looks like the file https://github.com/cloudblue/django-cqrs/blob/master/dj_cqrs/management/commands/utils.py should be moved somewhere out of the /commands folder

Fix `cqrs_dead_letters retry` command

Hey,

I've found that this Pull Request is breaking cqrs_dead_letters retry command.

_get_consumer_settings used to return 2 arguments and with this change it start to return tuple of 3,
which is breaking init_broker method in above mentioned command.
Exact line:

queue_name, dead_letter_queue_name = RabbitMQTransportService.get_consumer_settings()

is raising ValueError: too many values to unpack (expected 2)

Originally posted by @Szejdi in #39 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.