Re: Synchronous replication on push

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Sat, Nov 02, 2024 at 02:06:53AM +0000, Taylor R Campbell wrote:

> Whenever I push anything to it, I want the push -- that is, all the
> objects, and all the ref updates -- to be synchronously replicated to
> another remote repository, the back end:

This isn't quite how replication works at, say, GitHub. But let me first
explain some of what you're seeing, and then I'll give some higher level
comments at the end.

> Here are some approaches I've tried:
> 
> 1. `git clone --mirror -o backend git@xxxxxxxxxxxxxxxxxxx:/repo.git'
>    to create the front end repository, plus the following pre-receive
>    hook in the front end:
> 
> 	#!/bin/sh
> 	exec git push backend
> 
>    This doesn't work because the pre-receive hook runs in the
>    quarantine environment, and `git push' wants to update
>    `refs/heads/main', which is forbidden in the quarantine
>    environment.
> 
>    (However, git push to frontend doesn't actually fail with nonzero
>    exit status -- it prints an error message, `ref updates forbidden
>    inside quarantine environment', but exits wtih status 0.)
> 
>    But maybe the ref update is harmless in this environment.

I think the quarantine error is working as designed. If your push
updates local refs in the frontend repo, any object-existence checks it
does from the quarantine area are not necessarily valid if the
quarantine environment goes away without migrating the objects (e.g., if
you reject the push).

So this:

> 2. Same as (1), but the pre-receive hook is:
> 
> 	#!/bin/sh
> 	unset GIT_QUARANTINE_PATH
> 	exec git push backend

is potentially dangerous. Instead, you should disable push's attempt to
update the local tracking refs. There isn't an option to do that, but
if you don't have a "fetch" config line, then there are no tracking
refs. I.e., rather than using "clone --mirror", create your frontend
repo like this:

  git init --bare
  git config remote.backend.url git@xxxxxxxxxxxxxxxxxxx:/repo.git
  git fetch backend refs/*:refs/*

And then push won't try to update anything in the frontend repo.

  Side note: there's a small maybe-bug here that I noticed if the
  backend is on the same local filesystem. In that case
  GIT_QUARANTINE_PATH remains set for the receive-pack process running
  on the backend repo, and will refuse to update refs (where it should
  be safe to do so!). In your example that doesn't happen because
  GIT_QUARANTINE_PATH does not make it across the ssh connection. But
  arguably we should be clearing GIT_QUARANTINE_PATH in local_repo_env
  like we do for GIT_DIR, etc. I don't think you ran into this, but just
  another hiccup I found while trying to reproduce your situation.

Moving on...

>    This doesn't work because `git push' in the pre-receive hook
>    doesn't find anything it needs to push -- the ref update hasn't
>    happened yet.

Right. You could do it from a post-receive, but if the point is to be
able to reject the push to the frontend, it must happen before the refs
have been updated! So...

> 3. Same as (1), but the pre-receive hook assembles a command line of
> 
> 	exec git push backend ${new0}:${ref0} ${new1}:${ref1} ...,
> 
>    with all the ref updates passed on stdin (ignoring the old values).

...yes, this is the correct approach. You're not _quite_ passing all of
the relevant info, though, because you're ignoring the old value of each
ref. And ideally you'd make sure you were moving backend's ref0 from
"old0" to "new0"; otherwise you risk overwriting something that happened
independently on the backend. Of course that creates new questions,
like what happens when the frontend and backend get out of sync.

>    This fails because `--mirror can't be combined with refspecs'.

Yes. I don't think you really want "--mirror" in the first place, since
you won't be fetching from the backend (or will you? If you are, that
creates new questions about atomicity and syncing). If you do the
init+fetch above, it won't be set.

> 4. Same as (3), but remote.backend.mirror is explicitly disabled after
>    `git clone --mirror' finishes.
> 
>    On push to the primary, this prints an error message
> 
> 	remote: error: update_ref failed for ref 'refs/heads/main': ref updates forbidden inside quarantine environment
> 
>    but somehow the push succeeds in spite of this message, and the
>    primary and replica both get updated.

This is again the quarantine issue updating local tracking branches.
However, we don't consider that a hard error, as updating them is
opportunistic (we'd get the new values on the next fetch anyway).

If you drop the refspec as above, you shouldn't see that any more.

> 5. Same as (1), but the pre-receive hook assembles a command line of
> 
> 	exec git send-pack git@xxxxxxxxxxxxxxxxxxx:/repo.git \
> 		${new0}:${ref0} ${new1}:${ref1} ...
> 
>    with all the ref updates passed on stdin (ignoring the old values).
> 
>    This seems to work, and it propagates errors injected on push to
>    the replica, but it is limited to local or ssh remotes, as far as I
>    can tell -- it does not appear that git-send-pack works with custom
>    remote transports.

I don't remember all of the limitations of send-pack anymore. Even
though "push" is more porcelain than plumbing, I'd probably still
recommend it for a script, just because I think direct use of send-pack
isn't going to be all that exercised, so you are likely to find missing
bits of functionality and so forth. I think just dropping the refspecs
and using push would be following the more well-trodden path.


Now back to the main point: is this a good way to do replication? I
don't think it's _terrible_, but there are two flaws I can see:

  1. You're not kicking off the backend push until the frontend has
     received and processed the whole pack. So you're doubling the
     end-to-end latency of the push. In an ideal world you'd actually
     stream the incoming packfile to the backend, which would doing its
     own quarantined index-pack[*] on it in real-time. And then when you
     get to the pre-receive hook, all that's left is for all of the
     replicas to agree to commit to the ref update.

     [*] That would fix the latency, but of course you'd be spending a
     bunch of CPU on each replica to do the same indexing computation.
     You _could_ do that once, streaming the result out to the replicas,
     and then sending them just the resulting index. But there is some
     safety in repeating the computation on each replica (they _should_
     all have the same objects, but if that isn't the case, you'd notice
     if one of them was missing, say, a delta base that the others
     have). GitHub's original replication design did repeat the
     computation, and AFAIK that is still the case today.

  2. Using "push" isn't a very atomic way of updating refs. The backends
     will either accept the push or not, and then the frontend will try
     to update its refs. What if it fails? What if another push comes in
     simultaneously? Can they overwrite each other or lose pushed data?
     Or get the frontend and backends out of sync?

     Git's ref atomicity strategy is generally to take a lock on a ref,
     then check that its current value is the expected "old" value, and
     then update it to the "new" value and release the lock atomically.
     So you probably want to ask each backend replica to take the ref
     locks and check the old values, then respond "yes, I'm ready to
     commit", and then you send back "OK, commit" at which point they do
     the update.

     But "push" doesn't give you that kind of granularity (neither for
     the backends or on the frontend). Back when GitHub's replication
     system was designed, nothing did, and we had to use custom code.
     These days the reference-transaction lets you act in that stage
     where the ref lock is held (and my understanding is that GitLab
     implemented it to do the same kind of three-phase commit).

     But I don't have much experience with it myself. It might be
     enough if the frontend transaction hook talked to the backends,
     initiating an update-ref there with a transaction hook to pause and
     wait for the three-phase agreement.

Maybe some of that points you in the right direction.

-Peff




[Index of Archives]     [Linux Kernel Development]     [Gcc Help]     [IETF Annouce]     [DCCP]     [Netdev]     [Networking]     [Security]     [V4L]     [Bugtraq]     [Yosemite]     [MIPS Linux]     [ARM Linux]     [Linux Security]     [Linux RAID]     [Linux SCSI]     [Fedora Users]

  Powered by Linux