Coder Social home page Coder Social logo

kozmod / oniontx Goto Github PK

View Code? Open in Web Editor NEW
5.0 2.0 0.0 745 KB

The library for transferring transaction management to the application layer.

License: MIT License

Go 91.65% Makefile 6.90% Dockerfile 0.29% Shell 1.16%
go golang sql transaction clean-architecture onion-architecture tx repository delivery-layer hexagonal-architecture

oniontx's People

Contributors

dependabot[bot] avatar kozmod avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

oniontx's Issues

missing go.sum entry for go.mod file

Description

Bug when build locally:

0.823 main.go:11:2: github.com/jackc/pgx/[email protected]: missing go.sum entry for go.mod file; to add it:
0.823 	go mod download github.com/jackc/pgx/v5
0.823 main.go:12:2: github.com/jackc/pgx/[email protected]: missing go.sum entry for go.mod file; to add it:
0.823 	go mod download github.com/jackc/pgx/v5

Environment

  • go version: 1.22
  • oniontx version (version tag or commit sha):v0.3.7

fix multiple call `defer` when `Transactor` recursive call

When function WithinTx (WithinTxWithOpts) calls inside other WithinTx (WithinTxWithOpts) function (one or multiple times), defer calls every time when WithinTx (WithinTxWithOpts) finish

	defer func() {
		switch p := recover(); {
		case p != nil:
			if rbErr := tx.Rollback(ctx); rbErr != nil {
				err = xerrors.Errorf("transactor - panic [%v]: %w", p, errors.Join(rbErr, ErrRollbackFailed))
				return
			}
			err = xerrors.Errorf("transactor - panic [%v]: %w", p, ErrRollbackSuccess)
		case err != nil:
			if rbErr := tx.Rollback(ctx); rbErr != nil {
				err = xerrors.Errorf("transactor - call: %w", errors.Join(err, rbErr, ErrRollbackFailed))
				return
			}
			err = xerrors.Errorf("transactor - call: %w", errors.Join(err, ErrRollbackSuccess))
		default:
			if err = tx.Commit(ctx); err != nil {
				err = xerrors.Errorf("transactor: %w", errors.Join(err, ErrCommitFailed))
			}
		}
	}()

and Rollback or Commit functions may calls before top level's WithinTx (WithinTxWithOpts) function commit or rollback.

Only top level 's WithinTx (WithinTxWithOpts) has to have a control of the commit or rollback.

Down `go` version

Description

For build app with ald version of go need to down Transactor (and submodules version)

go: github.com/kozmod/oniontx/[email protected]: module github.com/kozmod/oniontx/[email protected] requires go >= 1.22 (running go 1.21.6)

go version: 1.21.6

Remove "task" issue section

Type (check all applicable)

  • Refactor
  • Feature
  • Optimization
  • Documentation Update

Description

"Type" section of bug_report.md define by github as "task" section
1)
Снимок экрана 2024-01-21 в 19 59 17
2)
Снимок экрана 2024-01-21 в 20 12 43

it is look like a bit unclear.

remove overhead for default `ContextOperator`

Current ContextOperator has the overhead on injecting/extracting Tx. Problem is the Key function ( convetation pointer's value to string's value 1️⃣):

// Inject returns new context.Context contains Tx as value.
func (p *ContextOperator[B, T]) Inject(ctx context.Context, tx T) context.Context {
	key := p.Key()  // 👈🏻
	return context.WithValue(ctx, key, tx)
}

// Extract returns Tx extracted from context.Context.
func (p *ContextOperator[B, T]) Extract(ctx context.Context) (T, bool) {
	key := p.Key() // 👈🏻
	c, ok := ctx.Value(key).(T)
	return c, ok
}

// Key returns key (CtxKey) for injecting or extracting Tx from context.Context.
func (p *ContextOperator[B, T]) Key() CtxKey {
	return CtxKey(fmt.Sprintf("%p", p.beginner)) // 1️⃣
}

According to benchmarks, overhead about x4 👇🏻

func Benchmark(bch *testing.B) {
	var (
		c = committerMock{}
		b = &beginnerMock[*committerMock, any]{}
	)

	benchmarks := []struct {
		name     string
		operator СtxOperator[*committerMock]
		fn       func(ctx context.Context, o СtxOperator[*committerMock]) context.Context
	}{
		{
			name:     "current",
			operator: NewContextOperator[*beginnerMock[*committerMock, any], *committerMock](&b),
			fn: func(ctx context.Context, o СtxOperator[*committerMock]) context.Context {
				c := o.Inject(ctx, &c)
				_, _ = o.Extract(ctx)
				return c
			},
		},
		{
			name:     "new",
			operator: NewContextOperatorV2[*beginnerMock[*committerMock, any], *committerMock](&b),
			fn: func(ctx context.Context, o СtxOperator[*committerMock]) context.Context {
				c := o.Inject(ctx, &c)
				_, _ = o.Extract(ctx)
				return c
			},
		},
	}
	for _, bm := range benchmarks {
		bch.Run(bm.name, func(b *testing.B) {
			var (
				ctx = context.Background()
				o   = bm.operator
			)
			bch.ReportAllocs()
			bch.ResetTimer()
			for i := 0; i < b.N; i++ {
				ctx = bm.fn(ctx, o)
			}
			_ = ctx
		})
	}
}
goos: darwin
goarch: amd64
pkg: github.com/kozmod/oniontx
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Benchmark
Benchmark/new
Benchmark/new-12         	11328888	       101.9 ns/op
Benchmark/current
Benchmark/current-12     	 3089184	       432.9 ns/op
PASS

because pointer is comparable, pointer's value will be able to using without convetation to string.

Add tests for `Transactor` recursive call

Description
Current coverage is

PASS
	github.com/kozmod/oniontx	coverage: 94.9% of statements
ok  	github.com/kozmod/oniontx	0.003s	coverage: 94.9% of statements
github.com/kozmod/oniontx/operator.go:13:	NewContextOperator	100.0%
github.com/kozmod/oniontx/operator.go:20:	Inject			100.0%
github.com/kozmod/oniontx/operator.go:25:	Extract			100.0%
github.com/kozmod/oniontx/transactor.go:50:	NewTransactor		100.0%
github.com/kozmod/oniontx/transactor.go:63:	WithinTx		100.0%
github.com/kozmod/oniontx/transactor.go:104:	WithinTxWithOpts	93.3%
github.com/kozmod/oniontx/transactor.go:163:	TryGetTx		100.0%
github.com/kozmod/oniontx/transactor.go:169:	TxBeginner		100.0%
total:						(statements)		94.9%

Need to add test for Transactor recursive call.

Set direct dependensies to submodule

Description

Error occurred

go: github.com/kozmod/[email protected]: module github.com/kozmod/[email protected] requires go >= 1.22 (running go 1.21.6)

because submodules have not direct version deps

module github.com/kozmod/oniontx/gorm

go 1.20

replace github.com/kozmod/oniontx => ../

require (
	github.com/kozmod/oniontx v0.3.1
	gorm.io/gorm v1.25.8
)

Environment

  • go version: 1.21
  • oniontx version (version tag or commit sha): 0.3.9

Generalise Transactor to use with no sеdlib libraries

Need to generalise the Transactor for using with different libs witch supported transactions (pix, sqlx, etc.).
It will be make using generic approach

type (
	TxBeginner[C TxCommitter, O any] interface {
		comparable
		BeginTx(ctx context.Context, opts ...Option[O]) (C, error)
	}

	TxCommitter interface {
		Rollback(ctx context.Context) error
		Commit(ctx context.Context) error
	}

	Option[TxOpt any] interface {
		Apply(in TxOpt)
	}

	СtxOperator[C TxCommitter] interface {
		Inject(ctx context.Context, c C) context.Context
		Extract(ctx context.Context) (C, bool)
	}
)
type Transactor[B TxBeginner[C, O], C TxCommitter, O any] struct {
	beginner B
	operator СtxOperator[C]
}

, or by another way.

Add `--force-recreate` to test env

Description

Change Makefile:

.PHONY: up
up: ## Up dest database
	COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml up 

.PHONY: up.d
up.d: ## Up dest database (detached)
	COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml up -d 

to

.PHONY: up
up: ## Up dest database
	COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml up --build --force-recreate

.PHONY: up.d
up.d: ## Up dest database (detached)
	COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml up -d --build --force-recreate

Change `key` accessing for default `ContextOperator`

Description

Default ContextOperator uses genetic type any(v0.2.3) (comparable #88 ) as key for storing/getting Tx.

Changing the key field to the function that return (calculate) the key, will make ContextOperator more flexible.

// ContextOperator inject and extract Tx from context.Context.
type ContextOperator[B comparable, T Tx] struct {
	key func() B
}

// NewContextOperator returns new ContextOperator.
func NewContextOperator[B comparable, T Tx](key func() B) *ContextOperator[B, T] {
	return &ContextOperator[B, T]{
		key: key,
	}
}

// Inject returns new context.Context contains Tx as value.
func (p *ContextOperator[B, T]) Inject(ctx context.Context, tx T) context.Context {
	return context.WithValue(ctx, p.key(), tx)
}

// Extract returns Tx extracted from context.Context.
func (p *ContextOperator[B, T]) Extract(ctx context.Context) (T, bool) {
	c, ok := ctx.Value(p.key()).(T)
	return c, ok
}

Update dependencies

Description

Update:

  1. oniontx/test/integration/migration
github.com/jackc/pgx/v4 v4.18.2 -> github.com/jackc/pgx/v5 v5.5.5
  1. oniontx/test
github.com/jackc/pgx/v5 v5.5.4 -> github.com/jackc/pgx/v5 v5.5.5
  1. oniontx/pgx
github.com/jackc/pgx/v5 v5.5.4 -> github.com/jackc/pgx/v5 v5.5.5

injecting Tx when Tx does not exist in the input context

In the current implementation СtxOperator inject transaction into context every call WithinTxWithOpts ( and WithinTx)

	defer func() {
		switch p := recover(); {
		case p != nil:
			if rbErr := tx.Rollback(ctx); rbErr != nil {
				err = xerrors.Errorf("transactor - panic [%v]: %w", p, errors.Join(rbErr, ErrRollbackFailed))
				return
			}
			err = xerrors.Errorf("transactor - panic [%v]: %w", p, ErrRollbackSuccess)
		case err != nil:
			if rbErr := tx.Rollback(ctx); rbErr != nil {
				err = xerrors.Errorf("transactor: %w", errors.Join(err, rbErr, ErrRollbackFailed))
				return
			}
			err = xerrors.Errorf("transactor: %w", errors.Join(err, ErrRollbackSuccess))
		default:
			if err = tx.Commit(ctx); err != nil {
				err = xerrors.Errorf("transactor: %w", errors.Join(err, ErrCommitFailed))
			}
		}
	}()

	ctx = t.operator.Inject(ctx, tx)  // 👈🏻 ❗️❗️❗️
	return fn(ctx)

and It has overhead (standard ContextOperator implementation, for example)

// Inject returns new context.Context contains Tx as value.
func (p *ContextOperator[B, T]) Inject(ctx context.Context, tx T) context.Context {
	key := p.Key()
	return context.WithValue(ctx, key, tx)
}

// Extract returns Tx extracted from context.Context.
func (p *ContextOperator[B, T]) Extract(ctx context.Context) (T, bool) {
	key := p.Key()
	c, ok := ctx.Value(key).(T)
	return c, ok
}

// Key returns key (CtxKey) for injecting or extracting Tx from context.Context.
func (p *ContextOperator[B, T]) Key() CtxKey {
	return CtxKey(fmt.Sprintf("%p", p.beginner))
}

👇🏻 Benchmarks👇🏻

func Benchmark_v1(b *testing.B) {
	const (
		k = "a"
		v = "x"
	)

	b.ResetTimer()
	b.ReportAllocs()
	var ctx context.Context = context.WithValue(context.Background(), k, v)
	for i := 0; i < b.N; i++ {
		_, ok := ctx.Value(k).(string)
		if !ok {
			ctx = context.WithValue(ctx, k, v)
		}
	}
	_ = ctx
}

func Benchmark_v2(b *testing.B) {
	const (
		k = "a"
		v = "x"
	)

	b.ResetTimer()
	b.ReportAllocs()
	var ctx context.Context = context.WithValue(context.Background(), k, v)
	for i := 0; i < b.N; i++ {
		c, _ := ctx.Value(k).(string)
		ctx = context.WithValue(ctx, k, c)
	}
	_ = ctx
}
pkg: github.com/kozmod/oniontx
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Benchmark_v1
Benchmark_v1-12    	187710049	         6.388 ns/op	       0 B/op	       0 allocs/op
Benchmark_v2
Benchmark_v2-12    	 9519942	       116.6 ns/op	      64 B/op	       2 allocs/op
PASS

reusing the existing check (on Tx in the input context ) helps to avoid overhead

	tx, ok := t.operator.Extract(ctx)
	if !ok {
		tx, err = t.beginner.BeginTx(ctx, opts...)
		if err != nil {
			return xerrors.Errorf("transactor - cannot begin: %w", errors.Join(err, ErrBeginTx))
		}
	}

	defer func() {
		switch p := recover(); {
// ...

	if !ok {
		ctx = t.operator.Inject(ctx, tx)
	}
	return fn(ctx)
}

fix goreleaser

Description

need fix

 error=failed to get module path: exit status 1: reading go.work: /home/runner/work/oniontx/oniontx/go.work:1: invalid go version '1.21.0': must match format 1.23

Fix `Readme.md` urls

Description

some URLs in the main Readme.md is was brocken.

Environment

  • go version: 1.22
  • oniontx version (version tag or commit sha): v0.3.1 (main)

add `TryExtractTransaction`

need to add Transaction.TryExtractTransaction function to have straight forward way get transaction from context.

separate options in `WithinTransaction`

function WithinTransaction

func (t *Transactor) WithinTransaction(ctx context.Context, fn func(ctx context.Context) error, options ...Option) (err error) { 
...

will have to be separate into two functions

func (t *Transactor) WithinOptionalTransaction(ctx context.Context, fn func(ctx context.Context) error, options ...Option) (err error) { 
...

func (t *Transactor) WithinTransaction(ctx context.Context, fn func(ctx context.Context) error) (err error) {
	return t.WithinOptionalTransaction(ctx, fn)
}

Add script to `release` action which creates submodules tags

Description

Need to add submodules tags automatically. Try to use actions/github-script@v7:

      - name: Set submodule tags
        uses: actions/github-script@v7
        with:
          script: |
            const creator = github.ref_name
            let submodules: string[] = ['stdlib', 'pgx', 'sqlx', 'gorm'];
            
            for (const submodule of submodules) {
              echo 'refs/tags/${{ ref_name }}'
            }

change `xerror`

remove xerror .Errorf(golang.org/x/xerrors) and use fmt.Errorf or errors.New.

improve structs and methods descriptions

Description

improve descriptions of types stdlib and main pkg structs and methods.

Environment

  • go version: 1.20 / 1.21
  • oniontx version (version tag or commit sha): 0.2.1

Add function to extract transaction from context

Need to add function to extract transaction from context.Context. It will be convenient to refactoring the code base.

func (r *Repository) Do(ctx context.Context, userID, postID, message string, interests []feed.UserInterest) (Some, error) {
	var value Some
	tx, err := r.db.BeginTx(ctx, &sql.TxOptions{})
	defer func() {
		switch p := recover(); {
		case p != nil:
			err = fmt.Errorf("repository: tx execute with panic: %v", p)
		case err != nil:
			_ = tx.Rollback()
		default:
			err = tx.Commit()
		}
	}()
	
		row := tx.QueryRowContext(ctx, `SELECT`)
		if err := row.Err(); err != nil {
			return value, fmt.Errorf("row: %w", err)
		}

		err = row.Scan(/* SCAN */)
		if err != nil {
			return value,fmt.Errorf("scan: %w", err)
		}

		_, err = tx.ExecContext(ctx, `UPDATE`)
		if err != nil {
			return value, fmt.Errorf("update posts quantity: %w", err)
		}

	return value, nil
}

👇🏻

func (r *Repository) Do(ctx context.Context) (Some, error) {
        tx, ok := ExtractTx
        if !ok {
		var value Some
		tx, err := r.db.BeginTx(ctx, &sql.TxOptions{})
		defer func() {
			switch p := recover(); {
			case p != nil:
				err = fmt.Errorf("repository: tx execute with panic: %v", p)
			case err != nil:
				_ = tx.Rollback()
			default:
				err = tx.Commit()
			}
		}()
	}
}

....

`Makefile` target to update `github.com/kozmod/oniontx` version in submodules

Description

need to make script to auto update github.com/kozmod/oniontx version in submodules.

example:

.PHONT: up.submods.deps
up.sm.deps:
	@(for sub in ${TAG_SUBMODULES} ; do \
		pushd $$sub && \
		sed -i '' 's/github.com\/kozmod\/oniontx v[^0-9]*\(\([0-9]\.\)\{0,4\}[0-9][^.]\)/github.com\/kozmod\/oniontx v1.1.1/g' go.mod && \
		popd; \
	done)

Simplify `stdlib.Transactor`

1️⃣ simplify methods TryGetTx and TxBeginner.
this methods returns wrappers of *sql.Tx and *sql.DB

// TryGetTx returns pointer of sql.Tx wrapper and "true" from context.Context or return `false`.
func (t *Transactor) TryGetTx(ctx context.Context) (*Tx, bool) {
	return t.transactor.TryGetTx(ctx)
}

// TxBeginner returns pointer of sql.DB wrapper.
func (t *Transactor) TxBeginner() *DB {
	return t.transactor.TxBeginner()
}

Its have to return simple only *sql.Tx and *sql.DB

// TryGetTx returns pointer of sql.Tx and "true" from context.Context or return `false`.
func (t *Transactor) TryGetTx(ctx context.Context) (*sql.Tx, bool) {
	wrapper, ok := t.transactor.TryGetTx(ctx)
	if !ok || wrapper == nil || wrapper.Tx == nil {
		return nil, false
	}
	return wrapper.Tx, true
}

// TxBeginner returns pointer of sql.DB.
func (t *Transactor) TxBeginner() *sql.DB {
	return t.transactor.TxBeginner().DB
}

2️⃣ make wrappers private to improve incopsulation

Adapt `Readmy.md`(and comments) to abstract usage

Description

Need to change the description of the project in Readmy.md to more abstract.

For Example:

NOTE: Transactor was developed to work with only the same instance of tha *sql.DB

to

NOTE: Transactor was developed to work with only the same instance of the "repository" (*sql.DB, etc.)

`goreleaser`: replace release notes

Type (check all applicable)

  • Refactor
  • Feature
  • Optimization
  • Documentation Update

Description

Set goreleaser config to replace release notes:

release:
  # What to do with the release notes in case there the release already exists.
  #
  # Valid options are:
  # - `keep-existing`: keep the existing notes
  # - `append`: append the current release notes to the existing notes
  # - `prepend`: prepend the current release notes to the existing notes
  # - `replace`: replace existing notes
  #
  # Default is `keep-existing`.
  mode: replace

Create `stdlib` default implementation

After generalization (#26)^ it will be nice to create common realization for stdlib:

type Transactor struct {
	transactor *oniontx.Transactor[*DB, *Tx, *sql.TxOptions]
	operator   *oniontx.ContextOperator[*DB, *Tx, *sql.TxOptions]
}

add `cancel ctx` test

Description

Need to add tests for pgx,sqlx,gorm with canceling ctx.

		var (
			ctx, cancel = context.WithCancel(context.Background())
			transactor  = ostdlib.NewTransactor(db)
			repositoryA = NewTextRepository(transactor, false)
			repositoryB = NewTextRepository(transactor, false)
			useCase     = NewUseCase(repositoryA, repositoryB, transactor)
		)

		cancel()
...

Update Makefile's `deps.update` target

Description

Now walk through dirs in the project and define them as submodules

.PHONT: deps.update
deps.update: ## Update dependencies versions (root and sub modules)
	@GOTOOLCHAIN=local go get -u all
	@go mod tidy
	@for d in */ ; do pushd "$$d" && go get -u all && go mod tidy && popd; done
	@go work sync

wiil be nice to use the variable for updating deps

SUBMODULES=stdlib pgx sqlx gorm

`ContextOperator.Inject`: key is not comparable

Description

BUG:
default ContextOperator use database (TxBeginner) for extracting and injecting ( Inject/Extract) transaction (Tx) to context.Context:

// ContextOperator inject and extract Tx from context.Context.
type ContextOperator[B any 👈🏻 , T Tx] struct {
	beginner *B
}

// NewContextOperator returns new ContextOperator.
func NewContextOperator[B any, T Tx](b *B) *ContextOperator[B, T] {
	return &ContextOperator[B, T]{
		beginner: b,
	}
}

// Inject returns new context.Context contains Tx as value.
func (p *ContextOperator[B, T]) Inject(ctx context.Context, tx T) context.Context {
	return context.WithValue(ctx, p.beginner, tx) 👈🏻
}

// Extract returns Tx extracted from context.Context.
func (p *ContextOperator[B, T]) Extract(ctx context.Context) (T, bool) {
	c, ok := ctx.Value(p.beginner).(T) 👈🏻
	return c, ok
}

if type B is not comparable, context.Context produce the panic: " key is not comparable"

To avoid the produce, need to change type B to comparable:

// ContextOperator inject and extract Tx from context.Context.
type ContextOperator[B comparable, T Tx] struct {
	beginner B
}

// NewContextOperator returns new ContextOperator.
func NewContextOperator[B comparable, T Tx](b B) *ContextOperator[B, T] {
	return &ContextOperator[B, T]{
		beginner: b,
	}
}

ENCHANCEMENT:
default ContextOperator use *B type in the constructor. It's a bit unhandy.

// NewContextOperator returns new ContextOperator.
func NewContextOperator[B any, T Tx](b *B 👈🏻) *ContextOperator[B, T] {
	return &ContextOperator[B, T]{
		beginner: b,
	}
}

the pointer may be removed.

Add `Makefile` targets

Type (check all applicable)

  • Refactor
  • Feature
  • Optimization
  • Documentation Update

Description

Targets:
1️⃣ show all coverage (add cover.out to .gitignore)
2️⃣ update all tools (goimports, etc.)

Remove unnecessary workflow steps

Description

Need to remove

    steps:
      - uses: actions/checkout@v3

      - name: Set up Go
        uses: actions/setup-go@v3
        with:
          go-version: 1.22.0

      - name: Run migrations // remove
        run: |
          go version

      - name: Run migrations
        run: |
          cd test/integration/migration
          go run . -cmd up -url "postgresql://test:passwd@localhost:5432/test?sslmode=disable"
      

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.