I'd like you to nitpick this idea I have to add server-less, multi-
process concurrency to Durus.
Goal: Allow more than one process and/or thread on a single machine
to reliably access and write to a Durus file, without having to
maintain a separate process for a Durus server, and without having to
install more Python packages.
I'll present several actions that a process or thread might take, and
how Durus would behave to support those actions.
Please let me know if you think this will work, any details or quirks
you can think of that might get in the way, etc. In return I will put
some time into this to see if I can produce a working patch.
== Lock file ==
Kept as a separate file, "mydatabase.durus.lock" being locked only
during write operations.
This is to allow all processes to at least read up to a "known good"
EOF marker while another process is writing a transaction.
== Packing ==
I haven't thought through this part as of yet, but I'm not worried
about it at the moment.
== Initial opening of a file ==
1. seek(SEEK_END), tell(), keep as current EOF offset
2. read initial state from file, create in-memory state
== Retrieve from a file that has not been updated ==
1. seek(SEEK_END), tell(), offset is the same as the current EOF offset
2. read requested object
== Retrieve from a file that has been updated ==
1. seek(SEEK_END), tell(), offset is different from the current EOF
offset
2. read records from current EOF offset to new EOF, update in-memory
state
3. read requested object
== commit() objects to a file that has not been updated ==
1. acquire exclusive lock
2. seek(SEEK_END), tell(), offset is the same as the current EOF offset
3. write records for new objects, update current EOF to new EOF
4. upon commit or rollback, release lock
== commit() objects to a file that has been updated ==
1. acquire exclusive lock
2. seek(SEEK_END), tell(), offset is different from current EOF offset
3. read records from current EOF offset to new EOF, update in-memory
state
a. if conflict, raise WriteConflictError
b. otherwise, write records for new objects, update current EOF to
new EOF
4. always, release lock
== Perform a consistent read across a data set ==
1. call pause() on the Durus connection
2. seek(SEEK_END), tell(), if offset is different from current EOF
offset, read records and update in-memory state
3. read requested objects, never doing seek/tell dance
... some time later ...
4. call continue() on the Durus connection
Thanks!
- Matt
- Each thread or process would open the file for itself, and keep its
own information about state.
- Two new methods would be added to a Schevo database:
- pause() would disable transaction execution but allow consistent
reads
- continue() would re-enable transaction execution but follow writes
made by other processes
- execute() would attempt to execute a transaction, retrying several
times (configurable) if a write lock or conflict occurs
David (and anyone else interested),
I'd like you to nitpick this idea I have to add server-less, multi-process concurrency to Durus.
The net improvement, then, is that you don't need to maintain a separate process for the durus server.
Maintaining the process seems like no inconvenience at all: am I right that it is having to *start* the
separate process that is the requirement that we would like to eliminate?
An alternative approach would be to have every client attempt to start a server process whenever
none is present, and this attempt will just fail for all except the first. Then the human management can avoid
thinking about the server process.
== Packing ==
I haven't thought through this part as of yet, but I'm not worried about it at the moment.
I seem to recall that this is one of the more difficult issues to solve.
It is important, too, so I'd suggest worrying about it now.
The other hard issue is oid allocation. ShelfStorage gets a big advantage
from having the entire space of oids be contained within a compact range
of values, so you can't just spread oids out. And of course, there must be
some way to make sure that no two clients use the same new oid.
== commit() objects to a file that has been updated ==
1. acquire exclusive lock
2. seek(SEEK_END), tell(), offset is different from current EOF offset
3. read records from current EOF offset to new EOF, update in-memory state
a. if conflict, raise WriteConflictError
This can be done, but it sounds easier than it really is. You'll need to
read the tail, find the oids, and make sure that none of them have states
loaded in your cache during this transaction. It isn't enough just to
look for conflict with the oids you are writing. This could potentially be
a slow operation, and it has a cost that the server-based durus avoids.
== Perform a consistent read across a data set ==
1. call pause() on the Durus connection
2. seek(SEEK_END), tell(), if offset is different from current EOF offset, read records and update in-memory state
You must also check for conflicts here and raise an exception if any of the loaded objects
have a new state.
Any time that the length of the file has changed, you must read the oids,
process them as invalidations, and raise the conflict exception if any of
the changed or new oids have state that is loaded into your memory.
> 1. epoll: Linux (2.6+) supports the epoll() system call, so that might
> be useful to implement some kind of polling/non-blocking io mecanism
> for
> accessing the Durus database file across multiple threads/processes.
The server side of Duruses uses cogen under the hood, which takes
advantage of epoll, kqueue, and iocp depending on platform and
availability.
http://code.google.com/p/cogen/
I tested various client/server combinations using Mac OS X 10.6,
Ubuntu 9.04, and Windows XP SP3, and was pleased to see that it works
fine across the whole spectrum.
For now, the client side is synchronous using built-in sockets, since
it matches the synchronous nature of the Schevo client API.
Multiple threads will be served by having thread-local connections,
e.g. maintain a worker threadpool where each thread will have its own
connection. This fits in with the current breed of WSGI servers nicely.
Multiple processes will simply need to create a connection to the
server and use it.
> 3. Durus user/group: Add support for a Schevo or Durus user/group on
> Linux. Maybe not immediately related to implementing a thread-safe
> Schevo but could be used to enforce Unix permissions, etc.
Because it's over the wire, I'm going to add some basic authentication
to the Duruses protocol before I do any public deployments.
Thanks for your input. I hope you'll be a beneficiary of this work. :)
Looking forward to checking out this in Schevo. Will this changes
anything to Schevo transaction methods and for opening database files ?