Intro
BK cannot allow duplicate delta keys to be created so on any given
machine/user combination the keys being created need to be monitored
and duplicates are prevented by changing the delta timestamp until the
shortKey (USER@HOST|PATH|DATE
) is unique.
The uniqdb is an mdbm where each db key is a delta shortKey and the db
value is the time_t
of its creation. When the delta’s date needs to be
fudged to create a unique key, its time_t
also is fudged. Note that
the db is not going to grow without bound - we are only interested
in keys which were created on this host, and for timestamp values in
the last CLOCK_DRIFT
seconds. We’ll start out with a CLOCK_DRIFT
of
48 hours and see if we need to shrink it. The 48 hours is because
sometimes people screw up their daylight savings time plus a generous
amount of slop.
In the previous design, a db in <dotbk>/bk-keys/HOST
was used (with a
fallback location of /tmp/.bk-keys-USER
if unwritable), with a lock
file in /tmp/.uniq_keys_USER
. When bk needed a unique delta key it
would call uniq_adjust()
which would consult the db and protect the
transaction with the lock file. This has the performance drawback of
taking and releasing the lock file on every bk instance that creates a
delta.
The new design moves uniqdb handling to a background info server which acquires the lock file only on startup.
Interoperability with bk6
The idea for interoperability with bk6 is for bk7 to maintain both bk6 and bk7’s uniqdbs by holding bk6’s lock file to lock out bk6 requests for delta keys when bk7’s uniq_daemon is running. This lets bk7 safely read and write both uniqdb’s to keep them consistent. Bk6’s uniqdb is read when bk7’s uniq_daemon starts up and is written out on exit.
To avoid locking bk6 out for too long, if bk6 has touched its uniqdb within the last day, then the uniq_daemon idle timeout is reduced to 10 seconds from 10 minutes. To track when to do this, bk7 will write a special delta key to bk6’s uniqdb with an old timestamp so that bk6 will always purge it but bk7 will leave it. The timestamp will encode the time that bk6 last wrote its uniqdb (Wayne suggested the file mod time minus one year). From this timestamp, bk7 can determine how long it should run with a reduced idle timeout of 10 seconds.
Here’s the logic for this on bk7 uniq_daemon startup: - if the bk6 uniqdb has no special key, the last-mod time of the file is when bk6 last wrote it. Write the special key with a timestamp of the last-mod time minus one year. Adjust the idle timeout based on this time. - if the special key is there, check whether we’re still within 24 hours of the encoded last-mod time and adjust the idle timeout accordingly.
This doesn’t help the case where someone starts up bk7 for a while, like for an import, and then tries to create a delta with bk6. In that case bk6 will be locked out until the bk7 uniq_daemon exits. During a staff meeting we agreed that this bk6 failure mode was acceptable.
Server
The uniq_daemon
server is started by bk when it needs a unique delta
key and the uniq_daemon isn’t already running. The server runs in
the background and waits for requests over a datagram socket (udp)
and exits after a certain time of inactivity.
The db file is stored in the following directory:
- if env var _BK_UNIQ_DIR
is defined, use that
- if “uniqdb” config option exists, use <config>/%HOST
- if /netbatch/.bk-%USER/bk-keys-db/%HOST
is writable, use that
abcdefghAO
- else use <dotbk>/bk-keys-db/%HOST
A lock file (named “lock”) and a file containing the uniq_daemon’s listening UDP port number (“port”) are stored along side the db file (“db”).
On startup, the lock file is used to ensure that at most one server instance is ever handling requests.
If the lock is available, it is acquired and then the server writes
the port
file and listens for requests. If --startsock=<port>
is
specified on the cmd line, the server makes a TCP connection to <port>
after the port
file is written to disk.
If the lock is busy, it means that another server instance already is
running and servicing requests, so the daemon exits without incurring
any db side effects. If --startsock=<port>
is specified, a TCP
connection still is made to <port>
, just before the second instance
exits.
Note
|
Should the second server verify the first server is actually
running? What if I killed -9 the first server and it left the db
locked, or does sccslock() already handle that?
|
So by using --startsock
, a client knows that, after it has received
the handshake from the server, a uniq_daemon
is running and its port
file has been written with the port on which it is listening.
There still is an exit race, where a valid port file can exist but the server instance which wrote it is on its shutdown path but has not yet removed its port file. In such a case, a client can acquire a connection to the server but then see a premature EOF on the socket and should retry the connection.
On exit, the server purges old delta keys and compacts the db (by copying the data to a fresh mdbm) if it has had over 1 MB of data and at least as much data has been deleted as added since the last compaction.
Client
BK determines whether there is a uniq_daemon
running by looking for
a port
file and if found, checking the output of sccs_stalelock()
to determine if the server is still running.
If a port
file is not found, or the lock
file is found to be
stale, a new server instance is spawned in the background.
Once the server is know to be running, bk contacts the uniq_daemon
by sending its queries to UDP 127.0.0.1:PORT
.
Each query is a single datagram with the following command:
insert-key\n@<SHORT KEY>\n<TIMESTAMP>\n@\n
Where <SHORT KEY>
is the simplified version of the key:
USER@HOST|PATH|DATE
, and <TIMESTAMP>
is the current time. The
client saves the md5 hash of this entire query.
The server’s response includes the md5 checksum of the question it is answering in the first line. in the above case, it would be:
<MD5> OK-<FUDGE>
If the server’s response’s MD5
doesn’t match the MD5
saved by the
client, the response is discarded and a new query is sent to the
server.
The client also implements its own timeout mechanism and will retry
contacting the server at least three times. In between each, it will
also check whether a server is present by double checking the port
file and calling sccs_stalelock()
on the lock again. If no server is
found, one will be started and the counter will be reset to 3.