Provides database replica pinning, round-robin read replica, allows unmanaged databases to be used side-by-side, and allows delegate routers to decide which DB set to use.

 TL;DR API:
         # Try to save without pinning:
         foo = Model.objects.all()[0]
         
         # UnpinnedWriteException raised under strict mode, or master pinning occurs under greedy mode.         
         foo.save()

         # read only from the master, allow writes.
         pindb.pin(alias)

         # Initialize/end the container:
         pindb.unpin_all()

         with unpinned_replica(alias):
            ... # read from replicas despite pinning state
         # or 
         queryset.using(pindb.get_replica('master-alias'))
 
         with master(alias):
            ... # write to master despite pinning state

Installation
    pip install pindb

    Add pindb.StrictPinDBRouter or pindb.GreedyPinDBRouter to DATABASE_ROUTERS.

    StrictPinDBRouter requires that pinning be declared before a write is attempted.  The advantage is that read replicas are used as much as possible.  The disadvantage is that your code will need many declarations to explicitly allow and opt out of the pinning.

    GreedyPinDBRouter will pin to a master as soon as a write occurs.  The advantage is that most of your code will just work.  The disadvantage is that you will use the read replicas less than possible.

    If you need to manage more than 1 master/replica set, add PINDB_DELEGATE_ROUTERS for pindb to defer to on DB set selection

    Define MASTER_DATABASES, same schema as DATABASES.

      DATABASES = {
        "unmanaged": {
          # HOST0, etc.
        }
      }

      MASTER_DATABASES = {
        "default": {
          # HOST1, etc.
        },
        "some_other_master": {
          ...
        }
      }

    Define DATABASE_SETS which overrides specific settings for replicas, e.g. 

      DATABASE_SETS = {
        "default": [{HOST:HOST1}, {HOST:HOST2}, ...],
        "some_other_master": [...] # zero or more replicas is fine.
      }

    Finalize DATABASES with pindb.populate_replicas:

      DATABASES.update(populate_replicas(MASTER_DATABASES, DATABASE_SETS))

    Add pindb.middleware.PinDBMiddleware to your MIDDLEWARE_CLASSES before any expected database access.

    Throughout your codebase, if you intend to write, declare that as early as possible to avoid inconsistent reads off the related replicas:

      pin("default")

    That will cause all future reads to use the master and allow writes to occur.

    To use under celery, hook celery.signals.task_postrun to pindb.unpin_all:

      import pindb
      from celery.signals import task_postrun

      def end_pinning(**kwargs):
        pindb.unpin_all()
      task_postrun.connect(end_pinning)

Further usage notes

    Exceptions raised:
      PinDBConfigError may be caused by:
        1) your settings do not include MASTER_DATABASES and DATABASE_SETS
        2) your MASTER_DATABASES do not include a "default" and populate_replicas is called without passing unmanaged_default=True.
        3) you declared an alias in MASTER_DATABASES which does not have a related DATABASE_SETS entry

      UnpinnedWriteException may be caused by:
        1) Model.objects.create, Model.save, qs.update, qs.delete without previously calling pindb.pin for the master

      Note that writes to unmanaged aliases (that is, ones unlisted in MASTER_DATABASES and related DATABASE_SETS) are allowed at any time.

      If you wish to read from a replica despite having previously pinned the master, you can do so with

        with pindb.unpinned_replica(alias):
          # code which reads from replicas

      If you wish to write to a master despite not having pinned to it, you can do so with:

        with pindb.master(alias):
          # code which writes to the db

Requirements/definitions:

      We have multiple separate masters (not necessarily sharded).  Let's call a grouping of a master and its replicas a "db set".

      We would like to have read replicas of these masters, and we would
like to read from replicas as much as possible and we would like all
writes to go to the master of the set.  But we would also like reads
to be consistent to writers.

      We would like this to be possible for web request cycles, but also
for units of work like tasks or shell scripts.  So we call this unit
of work the "pinning context".

      Writes to a given master should continue to read from the master to
avoid inconsistency in the replication lag window, so there will be an
API for declaring that.  Declaring (or otherwise preferring) that a
set master is needed is "pinning" and the group of pins for all db
sets is called the "pinned set".

      Code which plans on writing (or needs the very lastest data) should
be able to declare that as early as possible to get a fully-consistent
view from the master(s).

      It should be a clear error if we've made a mistake in pinning (that
is, writing after reading from a set).  The issue here is that if we
allow reads (not knowing that a write is coming) that gives an
inconsistency window.

               e.g  process reads from replica, gets a PK that has been deleted in
master, writes to master, fails.  Or gets a PK that's been mutated in
master so that it shouldn't have been processed, etc.

      Code which needs to write without pinning the whole container (e.g. a logging table) 
should be able to side-step the pinning.

      We should be able to manage the db sets in settings with minimal repetition and it 
should compose well with multiple settings files.
       

Solution:

       We use a threadlocal global to hold the pinned set.  This feels
icky, but until we need an evented models, passing around the pinned
set seems like a needless tax.  It would be possible to pass the pinned set around instead if we needed to support an evented execution model.

       The database router will then respect pinned set.

       The DATABASES dict in settings is "final" in the sense that it isn't
structured with any master/replica semantics.  So we use an intermediate
setting for defining sets:

               MASTER_DATABASES = {
                 'master-alias': { 'HOST':"a", ...normal settings },
                  ...
               }

               DATABASE_SETS = { 
                 'master-alias': [{'HOST':'someotherhost',...},], 
                  # override some of the master settings
               }

       And replica config can be finalized:

               DATABASES = populate_replicas(MASTER_DATABASES, DATABASE_SETS)

       resulting in something like:

               DATABASES = {
                 'master-alias': { 'HOST':"a", ...normal settings },
                 'master-alias-1': { 'HOST':"someotherhost", ...merged settings,
TEST_MIRROR='master-alias' },
               ...

      
       If no master is named "default", then the master of your first db set will also 
be aliased to "default".  You should use django.utils.datastructures.SortedDict to make that stable.

      In order to compose pinning with selection of the appropriate master, there is a one additional setting: DATABASE_ROUTER_DELEGATE.  It has the same interface as a normal DATABASE_ROUTER, but db_for_read and db_for_write must only return master aliases.  Then an appropriate master or replica will be chosen for that db set.

       The DB router will throw an error if db_for_write is called without
declaring that it's OK.  The correct approach is to pin the DB you
intend to do writes to *before you read* from a replica.

       To explictly prefer a read replica despite pinning, use either:

               with pindb.unpinned_replica('master-alias'):
                       ...

               or the .using method of a queryset.

       If you would like to explicitly use a replica, pindb.get_replica()
will return a replica alias.

       Pinning a set lasts the duration of a pinning context (once pinned, you can
not unpin a DB).  If you want to write to a DB without pinning the container, 
you can use queryset's .using method, which bypasses db_for_write.  Careful with this axe.

       To declare a pin:

               pindb.pin('master-alias')

For the common case of web requests and tasks, middleware and task
signals, respectively, will handle the call to
pindb.unpin_all()

TODO: Use signed cookies if available (dj 1.4+) for web pinning context.