Cleaning up unused packages

This commit is contained in:
Denis Arh
2020-11-02 14:23:46 +01:00
parent 02d18175f9
commit 8ba5e22d72
20 changed files with 1 additions and 1299 deletions
+1 -3
View File
@@ -16,6 +16,7 @@ require (
github.com/SentimensRG/ctx v0.0.0-20180729130232-0bfd988c655d
github.com/crusttech/go-oidc v0.0.0-20180918092017-982855dad3e1
github.com/davecgh/go-spew v1.1.1
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/disintegration/imaging v1.6.0
github.com/edwvee/exiffix v0.0.0-20180602190213-b57537c92a6b
@@ -43,7 +44,6 @@ require (
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0
github.com/lib/pq v1.1.0
github.com/markbates/goth v1.50.0
github.com/mattn/go-colorable v0.1.7 // indirect
github.com/mattn/go-sqlite3 v1.14.0
github.com/minio/minio-go/v6 v6.0.39
github.com/mitchellh/copystructure v1.0.0 // indirect
@@ -52,7 +52,6 @@ require (
github.com/pkg/errors v0.8.1
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
github.com/prometheus/client_golang v0.9.3
github.com/rakyll/gotest v0.0.5 // indirect
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009
@@ -63,7 +62,6 @@ require (
github.com/steinfletcher/apitest v1.3.8
github.com/steinfletcher/apitest-jsonpath v1.3.0
github.com/stretchr/testify v1.6.1
github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f
go.uber.org/atomic v1.6.0
go.uber.org/zap v1.15.0
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
-23
View File
@@ -63,8 +63,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v0.3.17 h1:NGWgggJJqTofUcTV1E7hkk2zVjZ54EfJa1z5O3z6By4=
@@ -83,8 +81,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@@ -180,15 +176,6 @@ github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
github.com/markbates/goth v1.50.0 h1:KCAErbDdHh11gQAJs/GV73LCv4NwA7Z6wNZAU32ggMc=
github.com/markbates/goth v1.50.0/go.mod h1:zZmAw0Es0Dpm7TT/4AdN14QrkiWLMrrU9Xei1o+/mdA=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.7 h1:bQGKb3vps/j0E9GfJQ03JyhRuxsvdAanXlT9BTw3mdw=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
@@ -238,8 +225,6 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rakyll/gotest v0.0.5 h1:+BrdqPxKPDaxvhtIiVzfiYXLhi4BrSOqdwaEiA7qjpk=
github.com/rakyll/gotest v0.0.5/go.mod h1:SkoesdNCWmiD4R2dljIUcfSnNdVZ12y8qK4ojDkc2Sc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
@@ -275,8 +260,6 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f h1:sTgdEUlmmNSU3QKe8n20jR26zYEKw6ciSuj919XobV4=
github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f/go.mod h1:VFd2XRrQZoX9cOxpeZezKpOlXDwU/dbRejKLjwP+xY8=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
@@ -340,7 +323,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -350,9 +332,6 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
@@ -408,8 +387,6 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.31.0 h1:T7P4R73V3SSDPhH7WW7ATbfViLtmamH0DKrP3f9AuDI=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.32.0 h1:zWTV+LMdc3kaiJMSTOFz2UgSBgx8RNQoTGiZu3fR9S0=
google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-32
View File
@@ -1,32 +0,0 @@
workspace:
base: /factory
kind: pipeline
name: crust
steps:
- name: build
image: golang:1.12-alpine
pull: always
environment:
CGO_ENABLED: 0
GOOS: linux
GOARCH: amd64
GOPROXY: https://proxy.golang.org
CI: travis
commands:
- go fmt ./...
- go test ./... -v -cover --tags="integration"
services:
- name: factory-db
pull: always
image: percona:8.0
command: [ "--innodb_lock_wait_timeout=2" ]
ports:
- 3306
environment:
MYSQL_ROOT_PASSWORD: bRxJ37sJ6Qu4
MYSQL_DATABASE: factory
MYSQL_USER: factory
MYSQL_PASSWORD: factory
-4
View File
@@ -1,4 +0,0 @@
/vendor
/coverage.*
/Gopkg.*
/.env
-14
View File
@@ -1,14 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2017+ Tit Petric <black@scene-si.org>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
-4
View File
@@ -1,4 +0,0 @@
.PHONY: all
all:
drone exec
-9
View File
@@ -1,9 +0,0 @@
# factory
Go factories for some common things which I use.
- Database (sqlx + mysql & pq) + utility functions
- Database query profiler for Stdout or Memory ([]string)
- Sonyflake ID generator for uint64 ids
Written by [@TitPetric](https://twitter.com/TitPetric) and licensed under the permissive [WTFPL](http://www.wtfpl.net/txt/copying/).
-578
View File
@@ -1,578 +0,0 @@
package factory
import (
"context"
"fmt"
"reflect"
"strings"
"time"
"database/sql"
"github.com/go-sql-driver/mysql"
_ "github.com/lib/pq"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/titpetric/factory/logger"
)
// DatabaseCredential is a configuration struct for a database connection
type DatabaseCredential struct {
DSN string
DriverName string
Connector func() (*sql.DB, error)
}
// DatabaseFactory contains all database credentials and instances
type DatabaseFactory struct {
credentials map[string]DatabaseCredential
instances map[string]*DB
}
// The default database factory
var Database *DatabaseFactory
func init() {
Database = &DatabaseFactory{}
Database.credentials = make(map[string]DatabaseCredential)
Database.instances = make(map[string]*DB)
}
// Add a new named database credential to the database factory
//
// The function will store a named database credential for use with
// the Get function. Generally you'd need to call Add at least once.
// In order to set the default database credential, use "default" as
// the first parameter.
//
// Example:
//
// ```
// factory.Database.Add("default", factory.DatabaseCredential{"mysql", "sqlapi:sqlapi@tcp(db1:3306)/sqlapi?collation=utf8mb4_general_ci"})
// ```
//
// By default, additional options will be added to the credentials DSN:
//
// - collation will be set to `utf8_general_ci`
// - parseTime will be set to `true`
// - loc will be set to `Local`
//
// If your passed DSN will include any of these options, the default values will not
// be applied, and your custom settings will be honored.
func (r *DatabaseFactory) Add(name string, credentials DatabaseCredential) {
credentials.DSN = r.CleanDSN(credentials.DriverName, credentials.DSN)
r.credentials[name] = credentials
}
func (r *DatabaseFactory) CleanDSN(driverName string, dsn string) string {
addOption := func(s, match, option string) string {
if !strings.Contains(s, match) {
s += option
}
return s
}
dsn = addOption(dsn, "?", "?")
dsn = addOption(dsn, "collation=", "&collation=utf8_general_ci")
dsn = addOption(dsn, "parseTime=", "&parseTime=true")
dsn = addOption(dsn, "loc=", "&loc=Local")
dsn = strings.Replace(dsn, "?&", "?", 1)
return dsn
}
// getCredentials returns the DatabaseCredential{} for a given db name
func (r *DatabaseFactory) getCredentials(name string) (*DatabaseCredential, error) {
if value, ok := r.credentials[name]; ok {
return &value, nil
}
return nil, errors.New("No configuration found for database: " + name)
}
// Get returns a database connection
//
// If you don't request a database connection by name, `factory.Database.Get()` will
// return the connection with the name "default". If you supply one or more names as
// the parameter, the first successful connection will be returned.
//
// It is fine to call this function per request, as a singleton instance is returned
// for each call with the same parameters.
//
// This behavior enables sharding workloads between hosts. One could randomize the
// parameters using [math/rand#Shuffle](https://tip.golang.org/pkg/math/rand/#Shuffle), or
// provide a consistent hashing method based on server hostname/IP, or even go so far
// to retrieve the hosts to connect to from some sort of inventory like etcd.
//
// The most general use case is that you will only call `factory.Database.Get()` once,
// and then pass the resulting `*DB` forward in your application. Requesting custom named
// connections also provides a way to access different parts of the database, or different
// database altogether, depending on your microservice data distribution.
func (r *DatabaseFactory) Get(dbName ...string) (*DB, error) {
names := dbName
if len(names) == 0 {
names = []string{"default"}
}
for _, name := range names {
if value, ok := r.instances[name]; ok {
return value, nil
}
credentials, err := r.getCredentials(name)
if err != nil {
return nil, err
}
if credentials.DriverName == "" {
return nil, errors.New("Credentials missing DriverName")
}
var handle *sqlx.DB
switch true {
case credentials.Connector != nil:
db, err := credentials.Connector()
if err != nil {
return nil, errors.WithStack(err)
}
handle = sqlx.NewDb(db, credentials.DriverName)
default:
handle, err = sqlx.Connect(credentials.DriverName, credentials.DSN)
if err != nil {
return nil, errors.WithStack(err)
}
}
r.instances[name] = &DB{
handle,
context.Background(),
0,
nil,
&sql.TxOptions{
ReadOnly: false,
},
logger.Silent{},
}
return r.instances[name], nil
}
return nil, fmt.Errorf("No configuration found for database: %v", names)
}
// MustGet is the same as Get, except it will panic on connection error
func (r *DatabaseFactory) MustGet(dbName ...string) *DB {
db, err := r.Get(dbName...)
if err != nil {
panic(err)
}
return db
}
// DB struct encapsulates sqlx.DB to add new functions
type DB struct {
*sqlx.DB
ctx context.Context
inTx int32
Tx *sqlx.Tx
TxOpts *sql.TxOptions
logger logger.Logger
}
// Quiet will return a DB handle without a logger
func (r *DB) Quiet() *DB {
return &DB{
r.DB,
r.ctx,
r.inTx,
r.Tx,
r.TxOpts,
logger.Silent{},
}
}
// With will return a DB handle with a bound context (throw-away)
func (r *DB) With(ctx context.Context) *DB {
return &DB{
r.DB,
ctx,
r.inTx,
r.Tx,
r.TxOpts,
r.logger,
}
}
// Set a custom logger
func (r *DB) SetLogger(logger logger.Logger) {
r.logger = logger
}
// Begin will create a transaction in the DB with a context
func (r *DB) Begin() (err error) {
if r.inTx > 0 {
_, err = r.Exec(fmt.Sprintf("SAVEPOINT sp_%d", r.inTx))
}
if r.inTx == 0 {
if r.ctx == nil {
r.Log(func() {
r.Tx, err = r.DB.Beginx()
}, "BEGIN;")
} else {
r.Log(func() {
r.Tx, err = r.DB.BeginTxx(r.ctx, r.TxOpts)
}, "BEGIN; -- with context")
}
}
if err != nil {
return errors.WithStack(err)
}
r.inTx++
return nil
}
// Transaction will create a transaction and invoke a callback
func (r *DB) Transaction(callback func() error) (err error) {
var try int
// Perform transaction statements
try = 0
for {
// Start transaction
if err = r.Begin(); err != nil {
return
}
if err = callback(); err == nil {
break
}
try++
if try > 3 {
r.logger.Log(r.ctx, fmt.Sprintf("Retried transaction %d times, aborting", try-1))
break
}
// Break out if the causer is not a MySQL error
cause, ok := (errors.Cause(err)).(*mysql.MySQLError)
if !ok {
r.logger.Log(r.ctx, "Returned error cause is not a MySQLError", logger.NewField("err", err))
break
}
// restart transaction:
// - 1205: lock within transaction (unit tested),
// - 1213: deadlock found
if cause.Number == 1205 || cause.Number == 1213 {
r.logger.Log(r.ctx, "Retrying transaction", logger.NewField("try", try), logger.NewField("cause", cause))
r.Rollback()
time.Sleep(50 * time.Millisecond)
continue
}
r.logger.Log(r.ctx, "Can't handle transaction error, aborting")
break
}
if err != nil {
r.Rollback()
return err
}
return r.Commit()
}
func (r *DB) Commit() (err error) {
if r.Tx != nil {
if r.inTx <= 1 {
r.Log(func() {
err = r.Tx.Commit()
}, "COMMIT;")
if err != nil {
return errors.WithStack(err)
}
r.Tx = nil
r.inTx = 0
return nil
}
if _, err = r.Exec(fmt.Sprintf("RELEASE SAVEPOINT sp_%d", r.inTx-1)); err != nil {
return errors.WithStack(err)
}
r.inTx--
return nil
}
return errors.WithStack(sql.ErrTxDone)
}
func (r *DB) Rollback() (err error) {
if r.Tx != nil {
if r.inTx <= 1 {
r.Log(func() {
err = r.Tx.Rollback()
}, "ROLLBACK;")
if err != nil {
return errors.WithStack(err)
}
r.Tx = nil
r.inTx = 0
return nil
}
if _, err = r.Exec(fmt.Sprintf("ROLLBACK SAVEPOINT sp_%d", r.inTx-1)); err != nil {
return errors.WithStack(err)
}
r.inTx--
return nil
}
return errors.WithStack(sql.ErrTxDone)
}
// SetFields will provide a string with SQL named bindings from a string slice
func (r *DB) SetFields(fields []string) string {
idx := 0
sql := ""
for _, field := range fields {
if idx > 0 {
sql = sql + ", "
}
idx++
sql = sql + field + "=:" + field
}
return sql
}
// NamedExec adds profiling on top of the parent DB.NameExec
func (r *DB) NamedExec(query string, arg interface{}) (res sql.Result, err error) {
exec := func() (sql.Result, error) {
if r.Tx != nil {
return r.Tx.NamedExecContext(r.ctx, query, arg)
}
return r.DB.NamedExecContext(r.ctx, query, arg)
}
r.Log(func() {
res, err = exec()
}, query, arg)
return res, errors.Wrap(err, "exec query failed")
}
// Exec adds profiling on top of the parent DB.Exec
func (r *DB) Exec(query string, args ...interface{}) (res sql.Result, err error) {
exec := func() (sql.Result, error) {
if r.Tx != nil {
return r.Tx.ExecContext(r.ctx, query, args...)
}
return r.DB.ExecContext(r.ctx, query, args...)
}
r.Log(func() {
res, err = exec()
}, query, args...)
return res, errors.Wrap(err, "exec query failed")
}
// Select is a helper function that will ignore sql.ErrNoRows
func (r *DB) Select(dest interface{}, query string, args ...interface{}) error {
var err error
exec := func() error {
if r.Tx != nil {
return r.Tx.SelectContext(r.ctx, dest, query, args...)
}
return r.DB.SelectContext(r.ctx, dest, query, args...)
}
r.Log(func() {
err = exec()
}, query, args...)
// clear no rows returned error
if err == sql.ErrNoRows {
return nil
}
return errors.Wrap(err, "select query failed")
}
// Get is a helper function that will ignore sql.ErrNoRows
func (r *DB) Get(dest interface{}, query string, args ...interface{}) error {
var err error
exec := func() error {
if r.Tx != nil {
return r.Tx.GetContext(r.ctx, dest, query, args...)
}
return r.DB.GetContext(r.ctx, dest, query, args...)
}
r.Log(func() {
err = exec()
}, query, args...)
// clear no rows returned error
if err == sql.ErrNoRows {
return nil
}
return errors.Wrap(err, "get query failed")
}
// set uses reflection to iterate over struct fields tags, producing bindings for struct values
func (r *DB) set(data interface{}, allowed ...string) string {
set := r.setMap(data, allowed...)
return r.setImplode(", ", set)
}
func (r *DB) tag(tag string) string {
if tag != "" && tag != "-" {
return strings.Split(tag, ",")[0]
}
return ""
}
func (r *DB) setMap(data interface{}, allowed ...string) map[string]string {
message_value := reflect.ValueOf(data)
if message_value.Kind() == reflect.Ptr {
message_value = message_value.Elem()
}
set := make(map[string]string)
length := message_value.NumField()
for i := 0; i < length; i++ {
fieldType := message_value.Type().Field(i)
if tag := r.tag(fieldType.Tag.Get("db")); tag != "" {
set[tag] = ":" + tag
}
}
// limit only to allowed fields
if len(allowed) > 0 {
for tag, _ := range set {
canDelete := true
for _, key := range allowed {
if tag == key {
canDelete = false
break
}
}
if canDelete {
delete(set, tag)
}
}
}
return set
}
func (r *DB) setImplode(delimiter string, set map[string]string) string {
result := ""
count := 0
for key, value := range set {
if count > 0 {
result = result + delimiter
}
result = result + key + "=" + value
count++
}
return result
}
// Update is a helper function which will issue an `update` statement to the db
func (r *DB) Update(table string, args interface{}, keys ...string) error {
var err error
if len(keys) == 0 {
return errors.New("Full-table update not supported")
}
set := r.setMap(args)
setWhere := make(map[string]string)
for _, key := range keys {
value, ok := set[key]
if !ok {
return errors.New("Can't update table " + table + " by key " + key + " (no such field in struct)")
}
delete(set, key)
setWhere[key] = value
}
if len(set) == 0 {
return errors.New("Encountered update struct with no fields")
}
query := "update " + table + " set " + r.setImplode(", ", set) + " where " + r.setImplode(" AND ", setWhere)
_, err = r.NamedExec(query, args)
return err
}
// UpdatePartial is a helper function which will issue an `update` statement to the db
func (r *DB) UpdatePartial(table string, args interface{}, allowed []string, keys ...string) error {
var err error
if len(keys) == 0 {
return errors.New("Full-table update not supported")
}
set := r.setMap(args, allowed...)
setWhere := make(map[string]string)
for _, key := range keys {
value, ok := set[key]
if !ok {
return errors.New("Can't update table " + table + " by key " + key + " (no such field in struct)")
}
delete(set, key)
setWhere[key] = value
}
if len(set) == 0 {
return errors.New("Encountered update struct with no fields")
}
query := "update " + table + " set " + r.setImplode(", ", set) + " where " + r.setImplode(" AND ", setWhere)
_, err = r.NamedExec(query, args)
return err
}
// Delete is a helper function which will issue an `delete` statement to the db
func (r *DB) Delete(table string, args interface{}, keys ...string) error {
var err error
if len(keys) == 0 {
return errors.New("Full-table delete not supported")
}
set := r.setMap(args)
setWhere := make(map[string]string)
for _, key := range keys {
value, ok := set[key]
if !ok {
return errors.New("Can't update table " + table + " by key " + key + " (no such field in struct)")
}
delete(set, key)
setWhere[key] = value
}
query := "delete from " + table + " where " + r.setImplode(" AND ", setWhere)
_, err = r.NamedExec(query, args)
return err
}
// Replace is a helper function which will issue an `replace` statement to the database
func (r *DB) Replace(table string, args interface{}) error {
var err error
query := "replace into " + table + " set " + r.set(args)
_, err = r.NamedExec(query, args)
return err
}
// Insert is a helper function which will issue an `insert` statement to the database
func (r *DB) Insert(table string, args interface{}) error {
var err error
query := "insert into " + table + " set " + r.set(args)
_, err = r.NamedExec(query, args)
return err
}
// InsertIgnore is a helper function which will issue an `insert ignore` statement to the database
func (r *DB) InsertIgnore(table string, args interface{}) error {
var err error
query := "insert ignore into " + table + " set " + r.set(args)
_, err = r.NamedExec(query, args)
return err
}
func (r *DB) Log(callback func(), query string, args ...interface{}) {
start := time.Now()
callback()
duration := time.Since(start).Seconds()
r.logger.Log(r.ctx, query, logger.NewField("duration", duration), logger.NewField("args", args))
}
-106
View File
@@ -1,106 +0,0 @@
package factory
import (
"context"
"fmt"
"log"
"regexp"
"time"
"github.com/pkg/errors"
"github.com/titpetric/factory/logger"
)
// DatabaseConnectionOptions is a configuration struct for connection retry
type DatabaseConnectionOptions struct {
DSN string
DriverName string
Logger string
Retries int
RetryTimeout time.Duration
ConnectTimeout time.Duration
}
var (
dsnMasker = regexp.MustCompile("(.)(?:.*)(.):(.)(?:.*)(.)@")
)
func (df *DatabaseFactory) TryToConnect(ctx context.Context, name string, options *DatabaseConnectionOptions) (db *DB, err error) {
df.Add(name, DatabaseCredential{
DSN: options.DSN,
DriverName: options.DriverName,
})
var (
connErrCh = make(chan error, 1)
)
// We'll not add this to the general log because we do not want to carry it with us for every query.
dsnField := fmt.Sprintf("dsn=%s", dsnMasker.ReplaceAllString(options.DSN, "$1****$2:$3****$4@"))
defer close(connErrCh)
log.Println("connecting to database", dsnField)
go func() {
var (
try = 0
)
for {
try++
if options.Retries <= try {
err = errors.Errorf("could not connect to %q, in %d tries", name, try)
return
}
db, err = df.Get(name)
if err != nil {
log.Println(
"could not connect to the database",
err,
dsnField,
fmt.Sprintf("try=%d", try),
)
select {
case <-ctx.Done():
// Forced break
break
case <-time.After(options.RetryTimeout):
// Wait before next try
continue
}
}
break
}
connErrCh <- err
}()
select {
case err = <-connErrCh:
break
case <-time.After(options.ConnectTimeout):
// Wait before next try
return nil, errors.Errorf("db init for %q timed out", name)
case <-ctx.Done():
return nil, errors.Errorf("db connection for %q cancelled", name)
}
switch options.Logger {
case "stdout":
db.SetLogger(logger.Default{})
default:
db.SetLogger(logger.Silent{})
}
if err != nil {
return nil, err
}
return db, nil
}
-12
View File
@@ -1,12 +0,0 @@
module github.com/titpetric/factory
go 1.12
require (
github.com/deckarep/golang-set v1.7.1 // indirect
github.com/go-sql-driver/mysql v1.4.1
github.com/jmoiron/sqlx v1.2.0
github.com/lib/pq v1.0.0
github.com/pkg/errors v0.8.1
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009
)
-15
View File
@@ -1,15 +0,0 @@
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009 h1:3wBL/e/qjpSYaXacpbIV+Bsj/nwQ4UO1llG/av54zzw=
github.com/sony/sonyflake v0.0.0-20181109022403-6d5bd6181009/go.mod h1:dVvZuWJd174umvm5g8CmZD6S2GWwHKtpK/0ZPHswuNo=
-20
View File
@@ -1,20 +0,0 @@
package logger
import (
"context"
"log"
)
type (
Default struct{}
)
func (Default) Log(ctx context.Context, msg string, fields ...Field) {
args := make([]interface{}, len(fields)+1)
args[0] = msg
for key, value := range fields {
args[key+1] = value
}
// fields satisfy fmt.Stringer as well
log.Println(args...)
}
-28
View File
@@ -1,28 +0,0 @@
package logger
import (
"fmt"
)
type (
field struct {
name string
value interface{}
}
)
func NewField(name string, value interface{}) Field {
return field{name, value}
}
func (f field) Name() string {
return f.name
}
func (f field) Value() interface{} {
return f.value
}
func (f field) String() string {
return fmt.Sprintf("%s=%v", f.name, f.value)
}
-23
View File
@@ -1,23 +0,0 @@
package logger
import (
"context"
"fmt"
)
type (
Logger interface {
Log(ctx context.Context, message string, fields ...Field)
}
Field interface {
Name() string
Value() interface{}
fmt.Stringer
}
)
func New() Logger {
return Default{}
}
-12
View File
@@ -1,12 +0,0 @@
package logger
import (
"context"
)
type (
Silent struct{}
)
func (Silent) Log(ctx context.Context, msg string, fields ...Field) {
}
-187
View File
@@ -1,187 +0,0 @@
# resputil
The package provides utilities to respond with some structured JSON payloads.
By default, anything you pass to `JSON` will be encapsulated depending on the type.
~~~go
func JSON(w http.ResponseWriter, responses ...interface{}) {
~~~
The `response` variadic parameter may be any of the following:
- string
- error
- int
- struct
- nil
- interface
- func() error
- func() (interface{}, error)
- func() ([]byte, error)
It will output the first non-empty value. In the case where the result is taken
from a `interface{}, error` return, it will first output the error if it's not
empty, and then output the value, *even if empty*. For all other cases it will
skip to the next item in the responses parameter.
A parameter of the type of `func() ([]byte, error)` will not return a wrapped structure.
It's assumed the `[]byte` result is an as-is payload which should be returned. A typical
use case is to return the output of `json.Marshal` which matches this signature.
The motivation behind it is to provide more reasonable error handling, when you
want to break out of your function with less code. It's in part an attempt to get
rid of all the `if err != nil {` checks in your code, but at the same time it's
also something that changes how your code might be laid in order to fully take
advantage of what it gives you.
For example, when writing APIs, you might structure your API call into several
logical units, that have different responsibilities:
- request validation (parameters)
- request processing (issuing SQL queries based on parameters)
- this one may be significantly broken down into many stages
- the actual response payload
Taking advantage of scope, this may look like this:
~~~go
mux.HandleFunc("/api/*", func(w http.ResponseWriter, r *http.Request) {
owner := login.Decode(r)
call := chi.URLParam(r, "*")
// validate request
validate := func() error {
if owner == "" {
return errors.New("Missing login info. Try to relogin")
}
if call == "" {
return errors.New("Unknown API call")
}
if r.Method == "POST" {
return errors.Wrap(r.ParseForm(), "Error parsing POST data")
}
return nil
}
// process request
process := func() (interface{}, error) {
params := map[string]interface{}{
"owner": owner,
}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
params[name] = param[0]
}
postVars := r.Form
for name, param := range postVars {
params[name] = param[0]
}
return sqlAPI(call, params)
}
// process request
resputil.JSON(w, validate, process)
})
~~~
Since `validate` and `process` are closures, they may access anything within the scope of
their parent function. This means that you can have a `RequestParameters` struct, a response
struct, and actually extend the logic of this further. This would be one possible way:
~~~go
// Parameters
params := &CommentListThread{
CommentList: &CommentList{
NewsID: chi.URLParam(r, "id"),
SessionID: r.URL.Query().Get("session_id"),
Sort: r.URL.Query().Get("sort"),
Order: r.URL.Query().Get("order"),
PageNumber: parseInt64(r.URL.Query().Get("pageNumber")),
PageSize: parseInt64(r.URL.Query().Get("pageSize")),
},
SelfID: parseInt64(r.URL.Query().Get("self_id")),
}
/* steps:
0. validate inputs
1. with self_id=0 parameters:
a. get comments with self_id 0 in the pagenumber/pagesize range,
b. get all child comments with parent comment IDs,
c. add 5 comments with date/asc to parent comments
d. return comments data
2. with self_id>0 parameters:
a. get comments with self_id X in the pagenumber/pagesize range,
b. return comments data
*/
// Parameters are included in the response
result := params
validate := func() error {
if !is(params.Sort, "date", "rating") {
params.Sort = "date"
}
if !is(params.Order, "asc", "desc") {
params.Order = "asc"
}
if params.PageNumber < 0 {
params.PageNumber = 0
}
if params.PageSize < 10 {
params.PageSize = 10
}
if params.PageSize > 100 {
params.PageSize = 100
}
return nil
}
// more code here ...
resputil.JSON(w, validate, process, addReplies, result)
~~~
The parent function is broken down into closures, that represent some stage of the issued
request. Depending on what works for you, each stage individually can produce an error using
a `func() error` declaration like shown here. If a non-empty value is returned, it will
be encoded into JSON and written to the HTTP output.
This pattern of use also allows a more functional approach to what you're responding with. For example,
if you favor something closer to an ORM approach, then you could do something more similar to this:
~~~go
func (p *ProjectHTTP) create(w http.ResponseWriter, r *http.Request) {
project := Project{}.New()
resputil.JSON(
w,
project.SetName(r.PostFormValue("name")),
project.Save(),
project,
)
}
~~~
In this case, both `SetName` and `Save` will be invoked, regardless of the fact if `SetName` returned
an error. Care should be taken between mixing `error` and `func() error` parameters, due to the order
in which they will be invoked. In the above case, both SetName and Save are invoked before `JSON()`, but
if we would omit `()` from `Save()`, the Save function would be invoked by `JSON()`.
## Tests
The package has 100% code coverage, but errors are possible. Due to the fact that the implementation is
aimed at generic "take anything" use, errors may occur at runtime.
## Other notes
- The package makes use of `pkg/errors`, returning a stack trace in the JSON response if configured with `SetConfig`
- There are helper functions `OK()` and `Success(string)` to format successful messages (`{ "success": { "message": "..." } }`)
- Errors will be formatted as `{ "error": { "message": "..." } }` [according to some Google conventions](https://cloud.google.com/storage/docs/json_api/v1/status-codes) which may or may not have a RFC
- HTTP response codes are not honored, you will always get a 200 OK response and a descriptive JSON payload
- valid responses are nested within `{ "response": ... }`
- if the set of all passed data to respond with it empty or all it's values are empty, `{ "response": false }` will be returned
## License
Written by [@TitPetric](https://twitter.com/TitPetric) and licensed under the permissive [WTFPL](http://www.wtfpl.net/txt/copying/).
-169
View File
@@ -1,169 +0,0 @@
package resputil
import (
"encoding/json"
"fmt"
"net/http"
"github.com/pkg/errors"
)
// Exported error messages
const (
E_EMPTY_TRACE = "no stack trace available"
)
type stackTracer interface {
StackTrace() errors.StackTrace
}
type successMessage struct {
Success struct {
Message string `json:"message"`
} `json:"success"`
}
type errorMessage struct {
Error struct {
Message string `json:"message"`
Trace string `json:"trace,omitempty"`
} `json:"error"`
}
// Options struct / configuration parameters
type Options struct {
Pretty bool // formats JSON output with indentation
Trace bool // prints a stack backtrace if exists (pkg/errors)
Logger func(error)
}
var config Options
// SetConfig updates package options in use
func SetConfig(options Options) {
config = options
}
// getTrace prints the first available stack trace if any
func getTrace(errs ...error) string {
for _, err := range errs {
if err != nil {
terr, ok := err.(stackTracer)
if ok {
return fmt.Sprintf("%+v", terr.StackTrace())
}
}
}
return E_EMPTY_TRACE
}
// error returns a structured error for API responses
func errorResponse(err ...error) errorMessage {
response := errorMessage{}
// add stack trace to the response if available and enabled
response.Error.Message = "Unknown error"
if len(err) > 0 {
if config.Trace {
response.Error.Trace = getTrace(errors.Cause(err[0]), err[0])
}
response.Error.Message = err[0].Error()
}
return response
}
// Success returns a structured success message for API responses
func Success(success ...string) successMessage {
response := successMessage{}
response.Success.Message = "OK"
if len(success) > 0 {
response.Success.Message = success[0]
}
return response
}
// OK returns the default Success message
func OK() successMessage {
return Success()
}
// JSON responds with the first non-nil payload, formats error messages
func JSON(w http.ResponseWriter, responses ...interface{}) {
respond := func(payload interface{}) {
var result []byte
var err error
encode := func(payload interface{}) ([]byte, error) {
if config.Pretty {
return json.MarshalIndent(payload, "", "\t")
}
return json.Marshal(payload)
}
switch value := payload.(type) {
case []byte:
result = value
case error:
// main key is "error"
errWithStack := errors.WithStack(value)
if config.Logger != nil {
config.Logger(errWithStack)
}
result, err = encode(errorResponse(errWithStack))
case successMessage:
// main key is "success"
result, err = encode(value)
default:
// main key is "response"
result, err = encode(struct {
Response interface{} `json:"response"`
}{value})
}
if err != nil {
result, _ = encode(errorResponse(errors.WithStack(err)))
}
w.Header().Set("Content-Type", "application/json")
w.Write(result)
}
for _, response := range responses {
switch value := response.(type) {
case nil:
// this will match a nil error
continue
case func() ([]byte, error):
result, err := value()
JSON(w, err, result)
case func() (interface{}, error):
result, err := value()
JSON(w, err, result)
case func() error:
err := value()
if err == nil {
continue
}
respond(err)
case *error:
if *value == nil {
continue
}
respond(*value)
case error:
respond(value)
case string:
if value == "" {
continue
}
respond(value)
case bool:
if !value {
continue
}
respond(value)
case successMessage:
respond(value)
default:
respond(value)
}
// Exit on the first output...
return
}
respond(false)
}
-20
View File
@@ -1,20 +0,0 @@
package factory
import (
"sync/atomic"
)
// Semaphore provides a run-once type of flag (atomic)
type Semaphore struct {
semaphore int32
}
// CanRun will allow a caller to proceed
func (l *Semaphore) CanRun() bool {
return atomic.CompareAndSwapInt32(&l.semaphore, 0, 1)
}
// Done finishes the run
func (l *Semaphore) Done() {
atomic.CompareAndSwapInt32(&l.semaphore, 1, 0)
}
-36
View File
@@ -1,36 +0,0 @@
package factory
import (
"github.com/sony/sonyflake"
"time"
)
// SonyflakeFactory is a configuration struct
type SonyflakeFactory struct {
*sonyflake.Sonyflake
}
// Sonyflake is the active ID generator instance
var Sonyflake *SonyflakeFactory
func init() {
Sonyflake = &SonyflakeFactory{
sonyflake.NewSonyflake(sonyflake.Settings{
StartTime: time.Unix(1503550784, 0),
}),
}
}
// NextID returns uint64 ID, escalates possible error to a panic
func (s *SonyflakeFactory) NextID() uint64 {
// sonyflake errors out only when the time overflows, that will
// occur in approximately 174 years after the custom epoch.
// If the 10ms keyspace is exhausted, NextID will sleep and return
// an ID from the next interval. It can't fail, because the generator
// is protected by a Mutex.
id, err := s.Sonyflake.NextID()
if err != nil {
panic(err)
}
return id
}
-4
View File
@@ -168,10 +168,6 @@ github.com/steinfletcher/apitest-jsonpath
# github.com/stretchr/testify v1.6.1
github.com/stretchr/testify/assert
github.com/stretchr/testify/require
# github.com/titpetric/factory v0.0.0-20190828134837-8466c9bef13f
github.com/titpetric/factory
github.com/titpetric/factory/logger
github.com/titpetric/factory/resputil
# go.uber.org/atomic v1.6.0
go.uber.org/atomic
# go.uber.org/multierr v1.5.0