Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redis adapter #33

Merged
merged 1 commit into from
Jan 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ jobs:
tests:
permissions:
pull-requests: write
services:
redis:
image: redis
ports:
- 6379:6379
# Set health checks to wait until redis has started
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
go-version:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Easiest way to get the perfect repository.
* [jmoiron/sqlx](https://github.com/jmoiron/sqlx), [docs](https://pkg.go.dev/github.com/avito-tech/go-transaction-manager/sqlx) (Go 1.13)
* [gorm](https://github.com/go-gorm/gorm), [docs](https://pkg.go.dev/github.com/avito-tech/go-transaction-manager/gorm) (Go 1.16)
* [mongo-go-driver](https://github.com/mongodb/mongo-go-driver), [docs](https://pkg.go.dev/github.com/avito-tech/go-transaction-manager/mongo) (Go 1.13)
* [go-redis/redis](https://github.com/go-redis/redis), [docs](https://pkg.go.dev/github.com/avito-tech/go-transaction-manager/redis) (Go 1.17)

## Installation

Expand All @@ -40,6 +41,7 @@ Compatibility beyond that is not guaranteed.
* [jmoiron/sqlx](sqlx/example_test.go)
* [gorm](gorm/example_test.go)
* [mongo-go-driver](mongo/example_test.go)
* [go-redis/redis](redis/example_test.go)

Below is an example how to start usage.

Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ go 1.13

require (
github.com/DATA-DOG/go-sqlmock v1.5.0
github.com/go-redis/redis/v8 v8.11.5
github.com/go-redis/redismock/v8 v8.0.7-0.20221230170314-95da4ad6c650
github.com/golang/mock v1.6.0
github.com/jinzhu/copier v0.3.5
github.com/jmoiron/sqlx v1.3.5
github.com/mattn/go-sqlite3 v1.14.16
github.com/onsi/gomega v1.24.1 // indirect
github.com/stretchr/testify v1.8.1
go.mongodb.org/mongo-driver v1.11.1
go.uber.org/multierr v1.9.0
Expand Down
140 changes: 135 additions & 5 deletions go.sum

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions gorm/settings.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build go1.16
// +build go1.16

package gorm

import (
Expand Down
3 changes: 3 additions & 0 deletions gorm/settings_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build go1.16
// +build go1.16

package gorm

import (
Expand Down
55 changes: 55 additions & 0 deletions internal/codegen/redis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
1. get readonly commands
```go
package main

import (
"context"
"fmt"
"log"

"github.com/go-redis/redis/v8"
)

func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

ctx := context.Background()
cmdsRes := rdb.Command(ctx)

var readonly []string

cc, err := cmdsRes.Result()
if err != nil {
log.Fatal(err)
}
for _, r := range cc {
if !r.ReadOnly {
continue
}

readonly = append(readonly, r.Name)
}

fmt.Println(readonly)
}
```
2. remove all lines by regexp `^(?!(?:del|set...)\().*`
3. replace by pattern

Find:
`^([^(]+)(\((?:((?:, )?\w+)(?: (?:[^,)]+))?)?(?:((?:, )?\w+)(?: (?:[^,)]+))?)?(?:((?:, )?\w+)(?: (?:[^,)]+))?)?(?:((?:, )?\w+)(?: (?:[^,)]+))?)?(?:((?:, )?\w+)(?: (?:[^,)]+))?)?\)) (\*?)(.+)`

Replace:
```regexp
func(p *WritePipeliner) $1$2 $8redis.$9 {\n\treturn p.read.$1($3$4$5$6$7)\n}\n
```
4. Set redis for arguments

Find: `(\w)\((.*\w )\*(\w+.*)\)`
replace: `$1($2*redis.$3)`
5. Set ... for arguments

Find: `(keys|members|pos|fields)\)`
replace: `$1...)`
2 changes: 1 addition & 1 deletion mongo/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func MustSettings(trms trm.Settings, oo ...Opt) Settings {
}

//revive:disable:exported
func (s Settings) EnrichBy(in trm.Settings) (res trm.Settings) { //nolint:ireturn,nolintlint
func (s Settings) EnrichBy(in trm.Settings) trm.Settings { //nolint:ireturn,nolintlint
external, ok := in.(Settings)
if ok {
if s.SessionOpts() == nil {
Expand Down
51 changes: 51 additions & 0 deletions redis/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//nolint:ireturn,nolintlint // return Tr for external usage.
//revive:disable:package-comments
package redis

import (
"context"

"github.com/go-redis/redis/v8"

"github.com/avito-tech/go-transaction-manager/trm"
trmcontext "github.com/avito-tech/go-transaction-manager/trm/context"
)

// DefaultCtxGetter is the CtxGetter with settings.DefaultCtxKey.
//
//nolint:gochecknoglobals
var DefaultCtxGetter = NewCtxGetter(trmcontext.DefaultManager)

// CtxGetter gets redis.Pipeliner from trm.СtxManager by casting trm.Transaction to redis.UniversalClient.
type CtxGetter struct {
ctxManager trm.СtxManager
}

//revive:disable:exported
func NewCtxGetter(c trm.СtxManager) *CtxGetter {
return &CtxGetter{ctxManager: c}
}

func (c *CtxGetter) DefaultTrOrDB(ctx context.Context, db redis.Cmdable) redis.Cmdable {
if tr := c.ctxManager.Default(ctx); tr != nil {
return c.convert(tr)
}

return db
}

func (c *CtxGetter) TrOrDB(ctx context.Context, key trm.CtxKey, db redis.Cmdable) redis.Cmdable {
if tr := c.ctxManager.ByKey(ctx, key); tr != nil {
return c.convert(tr)
}

return db
}

func (c *CtxGetter) convert(tr trm.Transaction) Cmdable {
if tx, ok := tr.Transaction().(Cmdable); ok {
return tx
}

return nil
}
182 changes: 182 additions & 0 deletions redis/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//go:build with_real_db
// +build with_real_db

package redis_test

import (
"context"
"encoding/json"
"fmt"

"github.com/go-redis/redis/v8"

trmredis "github.com/avito-tech/go-transaction-manager/redis"
"github.com/avito-tech/go-transaction-manager/trm"
"github.com/avito-tech/go-transaction-manager/trm/manager"
"github.com/avito-tech/go-transaction-manager/trm/settings"
)

// Example demonstrates the implementation of the Repository pattern by trm.Manager.
func Example() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})

ctx := context.Background()
rdb.FlushDB(ctx)

r := newRepo(rdb, trmredis.DefaultCtxGetter)

u1 := &user{
Username: "username1",
}
u2 := &user{
Username: "username2",
}

trManager := manager.Must(
trmredis.NewDefaultFactory(rdb),
manager.WithSettings(trmredis.MustSettings(
settings.Must(
settings.WithPropagation(trm.PropagationNested)),
trmredis.WithTxDecorator(trmredis.ReadOnlyFuncWithoutTxDecorator),
)),
)

err := r.Save(ctx, u1)
checkErr(err)

var cmds []redis.Cmder
err = trManager.DoWithSettings(
ctx,
trmredis.MustSettings(settings.Must(), trmredis.WithRet(&cmds)),
func(ctx context.Context) error {
if err := r.Save(ctx, u2); err != nil {
return err
}

u1FromDB, err := r.GetByID(ctx, u1.ID)
checkErr(err)

fmt.Println(u1FromDB)

return trManager.Do(ctx, func(ctx context.Context) error {
u2.Username = "new_username2"

return r.Save(ctx, u2)
})
},
)
checkErr(err)

fmt.Println("cmds:", len(cmds))

u2FromDB, err := r.GetByID(ctx, u2.ID)
checkErr(err)

fmt.Println(u2FromDB)
fmt.Println(rdb.DBSize(ctx))

// Output: &{6f2555ba-40a9-4fc8-90da-b968fd66f2e8 username1}
// cmds: 2
// &{0fa1769c-6e43-11ed-a1eb-0242ac120002 new_username2}
// dbsize: 2
}

type repo struct {
db redis.UniversalClient
getter *trmredis.CtxGetter
}

func newRepo(db redis.UniversalClient, c *trmredis.CtxGetter) *repo {
return &repo{
db: db,
getter: c,
}
}

const (
uuid1 uuid = "6f2555ba-40a9-4fc8-90da-b968fd66f2e8"
uuid2 uuid = "0fa1769c-6e43-11ed-a1eb-0242ac120002"
)

var currentUUID = 0

func newUUID() uuid {
res := []uuid{uuid1, uuid2}[currentUUID]

currentUUID++

return res
}

type uuid string

type user struct {
ID uuid
Username string
}

type userRecord struct {
Username string `redis:"username"`
}

func (r userRecord) MarshalBinary() (data []byte, err error) {
return json.Marshal(r)
}

func (r *userRecord) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, r)
}

func (r *repo) GetByID(ctx context.Context, id uuid) (*user, error) {
tx := r.getter.DefaultTrOrDB(ctx, r.db)
cmd := tx.Get(ctx, string(id))

var record userRecord

err := cmd.Scan(&record)
if err != nil {
return nil, err
}

return r.toModel(id, record), nil
}

func (r *repo) Save(ctx context.Context, u *user) error {
if u.ID == "" {
u.ID = newUUID()
}

cmd := r.getter.DefaultTrOrDB(ctx, r.db).Set(
ctx,
string(u.ID),
r.toRecord(u),
0,
)

if cmd.Err() != nil {
return cmd.Err()
}

return nil
}

func (r *repo) toRecord(model *user) userRecord {
return userRecord{
Username: model.Username,
}
}

func (r *repo) toModel(id uuid, row userRecord) *user {
return &user{
ID: id,
Username: row.Username,
}
}

func checkErr(err error) {
if err != nil {
panic(err)
}
}
18 changes: 18 additions & 0 deletions redis/factory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package redis

import (
"context"

"github.com/go-redis/redis/v8"

"github.com/avito-tech/go-transaction-manager/trm"
)

// NewDefaultFactory creates default trm.Transaction(redis.UniversalClient).
func NewDefaultFactory(db redis.UniversalClient) trm.TrFactory {
return func(ctx context.Context, trms trm.Settings) (context.Context, trm.Transaction, error) {
s, _ := trms.(Settings)

return NewTransaction(ctx, db, s)
}
}
Loading