Commit 65a81865 authored by Kirill Smelkov's avatar Kirill Smelkov

go/transaction: New package to deal with transactions (very draft)

Add Go counterpart of Python transaction package[1,2]. The Go version is
not complete - in particular Transaction.Commit is not yet implemented.
However even in this state it is useful to have transaction around for
read-only transaction cases.

The synchronization logic is more well-thought - in particular there is
no dance around "new transaction", because in Go, contrary to ZODB/py,
ZODB connections will be always opened under already started
transaction. See "Synchronization" section in package documentation for
details on this topic.

[1] http://transaction.readthedocs.org
[2] https://github.com/zopefoundation/transaction
parent 6d8e0d52
Zope Public License (ZPL) Version 2.1
A copyright notice accompanies this license document that identifies the
copyright holders.
This license has been certified as open source. It has also been designated as
GPL compatible by the Free Software Foundation (FSF).
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions in source code must retain the accompanying copyright
notice, this list of conditions, and the following disclaimer.
2. Redistributions in binary form must reproduce the accompanying copyright
notice, this list of conditions, and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Names of the copyright holders must not be used to endorse or promote
products derived from this software without prior written permission from the
copyright holders.
4. The right to distribute this software or to use it for any purpose does not
give you the right to use Servicemarks (sm) or Trademarks (tm) of the
copyright
holders. Use of them is covered by separate agreement with the copyright
holders.
5. If any files are modified, you must cause the modified files to carry
prominent notices stating that you changed the files and the date of any
change.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
This diff is collapsed.
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package transaction
import (
"context"
"sync"
//"lab.nexedi.com/kirr/go123/xerr"
)
// transaction implements Transaction.
type transaction struct {
mu sync.Mutex
status Status
datav []DataManager
syncv []Synchronizer
// metadata
user string
description string
extension string // XXX
}
// ctxKey is the type private to transaction package, used as key in contexts.
type ctxKey struct{}
// getTxn returns transaction associated with provided context.
// nil is returned if there is no association.
func getTxn(ctx context.Context) *transaction {
t := ctx.Value(ctxKey{})
if t == nil {
return nil
}
return t.(*transaction)
}
// currentTxn serves Current.
func currentTxn(ctx context.Context) Transaction {
txn := getTxn(ctx)
if txn == nil {
panic("transaction: no current transaction")
}
return txn
}
// newTxn serves New.
func newTxn(ctx context.Context) (Transaction, context.Context) {
if getTxn(ctx) != nil {
panic("transaction: new: nested transactions not supported")
}
txn := &transaction{status: Active}
txnCtx := context.WithValue(ctx, ctxKey{}, txn)
return txn, txnCtx
}
// Status implements Transaction.
func (txn *transaction) Status() Status {
txn.mu.Lock()
defer txn.mu.Unlock()
return txn.status
}
// Commit implements Transaction.
func (txn *transaction) Commit(ctx context.Context) error {
panic("TODO")
}
// Abort implements Transaction.
func (txn *transaction) Abort() {
var datav []DataManager
var syncv []Synchronizer
// under lock: change state to aborting; extract datav/syncv
func() {
txn.mu.Lock()
defer txn.mu.Unlock()
txn.checkNotYetCompleting("abort")
txn.status = Aborting
datav = txn.datav; txn.datav = nil
syncv = txn.syncv; txn.syncv = nil
}()
// lock released
// sync.BeforeCompletion -> errBeforeCompletion
n := len(syncv)
wg := sync.WaitGroup{}
wg.Add(n)
//errv := make([]error, n)
for i := 0; i < n; i++ {
i := i
go func() {
defer wg.Done()
syncv[i].BeforeCompletion(txn)
//errv[i] = syncv[i].BeforeCompletion(ctx, txn)
}()
}
wg.Wait()
//ev := xerr.Errorv{}
//for _, err := range errv {
// ev.Appendif(err)
//}
//errBeforeCompletion := ev.Err()
//xerr.Context(&errBeforeCompletion, "transaction: abort:")
// XXX if before completion = err -> skip data.Abort()? state -> AbortFailed?
// data.Abort
n = len(datav)
wg = sync.WaitGroup{}
wg.Add(n)
for i := 0; i < n; i++ {
i := i
go func() {
defer wg.Done()
datav[i].Abort(txn) // XXX err?
}()
}
wg.Wait()
// XXX set txn status
txn.mu.Lock()
// assert .status == Aborting
txn.status = Aborted // XXX what if errBeforeCompletion?
txn.mu.Unlock()
// sync.AfterCompletion
n = len(syncv)
wg = sync.WaitGroup{}
wg.Add(n)
for i := 0; i < n; i++ {
i := i
go func() {
defer wg.Done()
syncv[i].AfterCompletion(txn)
}()
}
// XXX return error?
}
// Join implements Transaction.
func (txn *transaction) Join(dm DataManager) {
txn.mu.Lock()
defer txn.mu.Unlock()
txn.checkNotYetCompleting("join")
// XXX forbid double join?
txn.datav = append(txn.datav, dm)
}
// RegisterSync implements Transaction.
func (txn *transaction) RegisterSync(sync Synchronizer) {
txn.mu.Lock()
defer txn.mu.Unlock()
txn.checkNotYetCompleting("register sync")
// XXX forbid double register?
txn.syncv = append(txn.syncv, sync)
}
// checkNotYetCompleting asserts that transaction completion has not yet began.
//
// and panics if the assert fails.
// must be called with .mu held.
func (txn *transaction) checkNotYetCompleting(who string) {
switch txn.status {
case Active: // XXX + Doomed ?
// ok
default:
panic("transaction: " + who + ": transaction completion already began")
}
}
// ---- meta ----
func (txn *transaction) User() string { return txn.user }
func (txn *transaction) Description() string { return txn.description }
func (txn *transaction) Extension() string { return txn.extension }
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// This program is free software: you can Use, Study, Modify and Redistribute
// it under the terms of the GNU General Public License version 3, or (at your
// option) any later version, as published by the Free Software Foundation.
//
// You can also Link and Combine this program with other software covered by
// the terms of any of the Free Software licenses or any of the Open Source
// Initiative approved licenses and Convey the resulting work. Corresponding
// source of such a combination shall include the source code for all other
// software used.
//
// This program is distributed WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
//
// See COPYING file for full licensing terms.
// See https://www.nexedi.com/licensing for rationale and options.
package transaction
import (
"context"
"sync/atomic"
"testing"
)
func TestBasic(t *testing.T) {
ctx := context.Background()
// Current(ø) -> panic
func() {
defer func() {
r := recover()
if r == nil {
t.Fatal("Current(ø) -> no panic")
}
if want := "transaction: no current transaction"; r != want {
t.Fatalf("Current(ø) -> %q; want %q", r, want)
}
}()
Current(ctx)
}()
// New
txn, ctx := New(ctx)
if txn_ := Current(ctx); txn_ != txn {
t.Fatalf("New inconsistent with Current: txn = %#v; txn_ = %#v", txn, txn_)
}
// New(!ø) -> panic
func() {
defer func() {
r := recover()
if r == nil {
t.Fatal("New(!ø) -> no panic")
}
if want := "transaction: new: nested transactions not supported"; r != want {
t.Fatalf("New(!ø) -> %q; want %q", r, want)
}
}()
_, _ = New(ctx)
}()
}
// DataManager that verifies abort path.
type dmAbortOnly struct {
t *testing.T
txn Transaction
nabort int32
}
func (d *dmAbortOnly) Modify() {
d.txn.Join(d)
}
func (d *dmAbortOnly) Abort(txn Transaction) {
if txn != d.txn {
d.t.Fatalf("abort: txn is different")
}
atomic.AddInt32(&d.nabort, +1)
}
func (d *dmAbortOnly) bug() { d.t.Fatal("must not be called on abort") }
func (d *dmAbortOnly) TPCBegin(_ Transaction) { d.bug(); panic(0) }
func (d *dmAbortOnly) Commit(_ context.Context, _ Transaction) error { d.bug(); panic(0) }
func (d *dmAbortOnly) TPCVote(_ context.Context, _ Transaction) error { d.bug(); panic(0) }
func (d *dmAbortOnly) TPCFinish(_ context.Context, _ Transaction) error { d.bug(); panic(0) }
func (d *dmAbortOnly) TPCAbort(_ context.Context, _ Transaction) { d.bug(); panic(0) }
func TestAbort(t *testing.T) {
txn, ctx := New(context.Background())
dm := &dmAbortOnly{t: t, txn: Current(ctx)}
dm.Modify()
// XXX +sync
txn.Abort()
if !(dm.nabort == 1 && txn.Status() == Aborted) {
t.Fatalf("abort: nabort=%d; txn.Status=%v", dm.nabort, txn.Status())
}
// Abort 2nd time -> panic
func() {
defer func() {
r := recover()
if r == nil {
t.Fatal("Abort2 -> no panic")
}
if want := "transaction: abort: transaction completion already began"; r != want {
t.Fatalf("Abort2 -> %q; want %q", r, want)
}
}()
txn.Abort()
}()
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment