server initial commit

This commit is contained in:
김도한 [dominick]
2022-10-18 11:50:10 +09:00
commit 8fe1f31bf2
10 changed files with 1899 additions and 0 deletions

322
shared/mongo.go Normal file
View File

@ -0,0 +1,322 @@
package shared
import (
"context"
"errors"
"flag"
"os"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
type MongoClient struct {
db *mongo.Database
c *mongo.Client
}
type ConnectionInfo struct {
Url string
Database string
}
var Devflag = flag.Bool("dev", false, "")
type CollectionName string
const (
CollectionCoupon = CollectionName("coupon")
CollectionCouponUse = CollectionName("coupon_use")
CollectionAccount = CollectionName("account")
CollectionAuth = CollectionName("auth")
CollectionMatch = CollectionName("match")
)
func mongourl() string {
v := os.Getenv("MONGO_URL")
if len(v) > 0 {
return v
}
return "mongodb://redis-dev.actionsquare.corp:27017/?replicaSet=repl01"
}
func NewMongoConnectionInfo() *ConnectionInfo {
if !flag.Parsed() {
flag.Parse()
}
dbname := "anvil"
if *Devflag {
dbname, _ = os.Hostname()
}
return &ConnectionInfo{
Url: mongourl(),
Database: dbname,
}
}
func (ci *ConnectionInfo) SetURL(url string) *ConnectionInfo {
ci.Url = url
return ci
}
func (ci *ConnectionInfo) SetDatabase(dbname string) *ConnectionInfo {
ci.Database = dbname
return ci
}
func NewMongoClient(ci *ConnectionInfo) (MongoClient, error) {
if len(ci.Url) == 0 {
return MongoClient{}, errors.New("mongo connection string is empty")
}
client, err := mongo.NewClient(options.Client().ApplyURI(ci.Url))
if err != nil {
return MongoClient{}, err
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err = client.Connect(ctx)
if err != nil {
return MongoClient{}, err
}
err = client.Ping(ctx, nil)
if err != nil {
return MongoClient{}, err
}
anvildb := client.Database(ci.Database, nil)
makeExpiredIndex := func(collname CollectionName, expireSeconds int32) error {
matchcoll := anvildb.Collection(string(collname))
indices, err := matchcoll.Indexes().List(ctx, options.ListIndexes().SetMaxTime(time.Second))
if err != nil {
return err
}
allindices := make([]interface{}, 0)
err = indices.All(context.Background(), &allindices)
if err != nil {
return err
}
tsfound := false
var tsname string
var exp int32
for _, index := range allindices {
d := index.(bson.D)
key := d.Map()["key"].(bson.D)
for _, kd := range key {
if kd.Key == "_ts" {
tsfound = true
}
}
if v, ok := d.Map()["name"]; ok {
tsname = v.(string)
}
if v, ok := d.Map()["expireAfterSeconds"]; ok {
exp = v.(int32)
}
}
if tsfound {
if exp == expireSeconds {
return nil
}
_, err = matchcoll.Indexes().DropOne(ctx, tsname)
if err != nil {
return err
}
}
mod := mongo.IndexModel{
Keys: primitive.M{"_ts": 1},
Options: options.Index().SetExpireAfterSeconds(expireSeconds),
}
_, err = matchcoll.Indexes().CreateOne(ctx, mod)
return err
}
if err = makeExpiredIndex(CollectionMatch, 30); err != nil {
return MongoClient{}, err
}
if err = makeExpiredIndex(CollectionAuth, 300); err != nil {
return MongoClient{}, err
}
return MongoClient{c: client, db: anvildb}, nil
}
func (mc MongoClient) Close() {
if mc.c != nil {
mc.c.Disconnect(context.Background())
}
}
func (mc MongoClient) Watch(coll CollectionName, pipeline mongo.Pipeline) (*mongo.ChangeStream, error) {
return mc.Collection(coll).Watch(context.Background(), pipeline, options.ChangeStream().SetFullDocument(options.UpdateLookup).SetMaxAwaitTime(0))
}
func (mc MongoClient) Collection(collname CollectionName) *mongo.Collection {
return mc.db.Collection(string(collname))
}
func (mc MongoClient) All(coll CollectionName, opts ...*options.FindOptions) ([]bson.M, error) {
cursor, err := mc.Collection(coll).Find(context.Background(), bson.D{}, opts...)
if err != nil {
return nil, err
}
var all []bson.M
err = cursor.All(context.Background(), &all)
if err != nil {
return nil, err
}
return all, nil
}
func (mc MongoClient) FindOneAndDelete(coll CollectionName, filter bson.M) (bson.M, error) {
result := mc.Collection(coll).FindOneAndDelete(context.Background(), filter)
err := result.Err()
if err != nil {
if err == mongo.ErrNoDocuments {
return nil, nil
}
return nil, err
}
tmp := make(map[string]interface{})
err = result.Decode(&tmp)
if err != nil {
return nil, err
}
return bson.M(tmp), nil
}
func (mc MongoClient) Delete(coll CollectionName, filter bson.M) (bool, error) {
r, err := mc.Collection(coll).DeleteOne(context.Background(), filter)
if err != nil {
return false, err
}
return r.DeletedCount > 0, nil
}
func (mc MongoClient) UnsetField(coll CollectionName, filter bson.M, doc bson.M) error {
_, err := mc.Collection(coll).UpdateOne(context.Background(), filter, bson.M{
"$unset": doc,
})
return err
}
func (mc MongoClient) DeleteMany(coll CollectionName, filters bson.M, opts ...*options.DeleteOptions) (int, error) {
result, err := mc.Collection(coll).DeleteMany(context.Background(), filters, opts...)
if err != nil {
return 0, err
}
return int(result.DeletedCount), nil
}
func (mc MongoClient) InsertMany(coll CollectionName, documents []interface{}, opts ...*options.InsertManyOptions) (int, error) {
result, err := mc.Collection(coll).InsertMany(context.Background(), documents, opts...)
if err != nil {
return 0, err
}
return len(result.InsertedIDs), nil
}
func (mc MongoClient) UpdateMany(coll CollectionName, filter bson.M, doc bson.M, opts ...*options.UpdateOptions) (count int, err error) {
result, e := mc.Collection(coll).UpdateMany(context.Background(), filter, doc, opts...)
if e != nil {
return 0, e
}
err = nil
count = int(result.UpsertedCount + result.ModifiedCount)
return
}
func (mc MongoClient) Update(coll CollectionName, filter bson.M, doc bson.M, opts ...*options.UpdateOptions) (worked bool, newid interface{}, err error) {
result, e := mc.Collection(coll).UpdateOne(context.Background(), filter, doc, opts...)
if e != nil {
return false, "", e
}
err = nil
worked = result.UpsertedCount > 0 || result.ModifiedCount > 0
newid = result.UpsertedID
return
}
func (mc MongoClient) UpsertOne(coll CollectionName, filter bson.M, doc bson.M) (worked bool, newid interface{}, err error) {
return mc.Update(coll, filter, bson.M{
"$set": doc,
}, options.Update().SetUpsert(true))
}
func (mc MongoClient) FindOne(coll CollectionName, filter bson.M, opts ...*options.FindOneOptions) (doc bson.M, err error) {
result := mc.Collection(coll).FindOne(context.Background(), filter, opts...)
tmp := make(map[string]interface{})
err = result.Decode(&tmp)
if err == nil {
doc = bson.M(tmp)
} else if err == mongo.ErrNoDocuments {
err = nil
}
return
}
func (mc MongoClient) FindOneAndUpdate(coll CollectionName, filter bson.M, doc bson.M, opts ...*options.FindOneAndUpdateOptions) (olddoc bson.M, err error) {
result := mc.Collection(coll).FindOneAndUpdate(context.Background(), filter, doc, opts...)
tmp := make(map[string]interface{})
err = result.Decode(&tmp)
if err == nil {
olddoc = bson.M(tmp)
} else if err == mongo.ErrNoDocuments {
err = nil
}
return
}
func (mc MongoClient) Exists(coll CollectionName, filter bson.M) (bool, error) {
cnt, err := mc.Collection(coll).CountDocuments(context.Background(), filter, options.Count().SetLimit(1))
if err != nil {
return false, err
}
return cnt > 0, nil
}
func (mc MongoClient) FindAll(coll CollectionName, filter bson.M, opts ...*options.FindOptions) ([]bson.M, error) {
cursor, err := mc.Collection(coll).Find(context.Background(), filter, opts...)
if err != nil {
return nil, err
}
output := make([]interface{}, 0)
err = cursor.All(context.Background(), &output)
if err != nil {
return nil, err
}
docs := make([]bson.M, 0, len(output))
for _, doc := range output {
one := make(bson.M)
for _, kv := range doc.(bson.D) {
one[kv.Key] = kv.Value
}
docs = append(docs, one)
}
return docs, nil
}

381
shared/server.go Normal file
View File

@ -0,0 +1,381 @@
package shared
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/signal"
"reflect"
"runtime/debug"
"strconv"
"strings"
"sync/atomic"
"syscall"
)
const (
// HTTPStatusReloginRequired : http status를 이걸 받으면 클라이언트는 로그아웃하고 로그인 화면으로 돌아가야 한다.
HTTPStatusReloginRequired = 599
// HTTPStatusReloginRequiredDupID :
HTTPStatusReloginRequiredDupID = 598
// HTTPStatusPlayerBlocked :
HTTPStatusPlayerBlocked = 597
)
type ShutdownFlag int32
const (
ShutdownFlagRunning = ShutdownFlag(0)
ShutdownFlagTerminating = ShutdownFlag(1)
ShutdownFlagRestarting = ShutdownFlag(2)
ShutdownFlagIdle = ShutdownFlag(3)
)
type functionCallContext struct {
Method string
Args []interface{}
}
// RPCReturnType : RPC 호출 가능한 함수가 유일하게 리턴할 수 있는 타입
type RPCReturnType interface {
Serialize(io.Writer) error
Error() error
}
type rpcReturnTypeImpl struct {
value reflect.Value
err error
serialized []byte
}
// Bytes : RPCReturnType.Serialize 구현
func (r rpcReturnTypeImpl) Serialize(w io.Writer) (err error) {
err = r.err
if err != nil {
return err
}
if len(r.serialized) > 0 {
w.Write(r.serialized)
return nil
}
if !r.value.IsValid() {
return nil
}
switch r.value.Kind() {
case reflect.String:
_, err = w.Write([]byte(r.value.String()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
_, err = w.Write([]byte(fmt.Sprintf("%d", r.value.Int())))
case reflect.Float32, reflect.Float64:
_, err = w.Write([]byte(fmt.Sprintf("%f", r.value.Float())))
case reflect.Slice:
switch r.value.Type().Elem().Kind() {
case reflect.Uint8:
_, err = w.Write(r.value.Bytes())
default:
var conv []interface{}
for i := 0; i < r.value.Len(); i++ {
conv = append(conv, r.value.Index(i).Interface())
}
if len(conv) == 0 {
_, err = w.Write([]byte("[]"))
} else {
err = json.NewEncoder(w).Encode(conv)
}
}
case reflect.Interface, reflect.Struct, reflect.Map:
err = json.NewEncoder(w).Encode(r.value.Interface())
case reflect.Ptr:
if !r.value.IsNil() {
err = json.NewEncoder(w).Encode(r.value.Interface())
}
}
return err
}
// Error : RPCReturnType.Error 구현
func (r rpcReturnTypeImpl) Error() error {
return r.err
}
// MakeRPCReturn :
func MakeRPCReturn(value interface{}, err error) RPCReturnType {
return rpcReturnTypeImpl{
value: reflect.ValueOf(value),
err: err,
}
}
// MakeRPCReturnSerialized : 이미 시리얼라이즈 한 Return
func MakeRPCReturnSerialized(serialized []byte) RPCReturnType {
return rpcReturnTypeImpl{
serialized: serialized,
}
}
// MakeRPCError :
func MakeRPCError(args ...interface{}) RPCReturnType {
return rpcReturnTypeImpl{
value: reflect.ValueOf(nil),
err: errors.New(fmt.Sprint(args...)),
}
}
// MakeRPCErrorf :
func MakeRPCErrorf(format string, v ...interface{}) RPCReturnType {
return rpcReturnTypeImpl{
value: reflect.ValueOf(nil),
err: fmt.Errorf(format, v...),
}
}
// Server :
type Server struct {
httpserver *http.Server
shutdownFlag ShutdownFlag
preShutdown func()
stopfunc func()
}
var prefixptr = flag.String("prefix", "", "'")
var portptr = flag.Int("port", 80, "")
var tls = flag.String("tls", "", "")
func welcomeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("welcome"))
// welcome은 accesslog 안 남김
// if atomic.LoadInt32(&serveMux.Shutdowning) == 0 {
// w.Write([]byte("welcome"))
// } else {
// w.WriteHeader(http.StatusServiceUnavailable)
// }
io.Copy(ioutil.Discard, r.Body)
r.Body.Close()
}
func NewDependentServer(preShutdown func()) *Server {
server := &Server{
httpserver: nil,
shutdownFlag: ShutdownFlagIdle,
preShutdown: preShutdown,
}
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
go func() {
<-c
signal.Stop(c)
server.Shutdown(ShutdownFlagTerminating)
}()
return server
}
// NewHTTPServer :
func NewHTTPServer(serveMux *http.ServeMux, preShutdown func()) *Server {
server := NewDependentServer(preShutdown)
// 시작시 자동으로 enable됨
if len(*tls) > 0 && *portptr == 80 {
*portptr = 443
}
addr := fmt.Sprintf(":%d", *portptr)
serveMux.HandleFunc("/welcome", welcomeHandler)
server.httpserver = &http.Server{Addr: addr, Handler: serveMux}
server.httpserver.SetKeepAlivesEnabled(true)
return server
}
func (server *Server) ShutdownFlag() ShutdownFlag {
return ShutdownFlag(atomic.LoadInt32((*int32)(&server.shutdownFlag)))
}
// Shutdown :
func (server *Server) Shutdown(flag ShutdownFlag) {
atomic.StoreInt32((*int32)(&server.shutdownFlag), int32(flag))
if server.preShutdown != nil {
server.preShutdown()
}
if server.httpserver != nil {
server.httpserver.Shutdown(context.Background())
server.httpserver = nil
} else if server.stopfunc != nil {
server.stopfunc()
server.stopfunc = nil
}
}
// Start :
func (server *Server) Start() {
if !flag.Parsed() {
flag.Parse()
}
atomic.StoreInt32((*int32)(&server.shutdownFlag), int32(ShutdownFlagRunning))
if server.httpserver != nil {
var err error
if len(*tls) > 0 {
crtfile := *tls + ".crt"
keyfile := *tls + ".key"
err = server.httpserver.ListenAndServeTLS(crtfile, keyfile)
} else {
err = server.httpserver.ListenAndServe()
}
if err != http.ErrServerClosed {
fmt.Println(err)
}
} else {
ctx, stopfunc := context.WithCancel(context.Background())
server.stopfunc = stopfunc
<-ctx.Done()
}
}
// ConvertInterface :
func ConvertInterface(from interface{}, toType reflect.Type) reflect.Value {
if toType.Kind() == reflect.String {
return reflect.ValueOf(from.(string))
}
fromrv := reflect.ValueOf(from)
fromtype := reflect.TypeOf(from)
if toType.Kind() == reflect.Struct {
if frommap, ok := from.(map[string]interface{}); ok {
out := reflect.New(toType)
for k, v := range frommap {
if fieldval := out.Elem().FieldByName(k); fieldval.IsValid() {
fieldval.Set(ConvertInterface(v, fieldval.Type()))
}
}
return out
}
}
if fromtype.Kind() == reflect.Slice {
if fromtype.Elem() == toType.Elem() {
return fromrv
}
convslice := reflect.MakeSlice(toType, 0, fromrv.Len())
for i := 0; i < fromrv.Len(); i++ {
convslice = reflect.Append(convslice, ConvertInterface(fromrv.Index(i).Interface(), toType.Elem()))
}
return convslice
}
if toType.Kind() == reflect.Bool {
val, _ := strconv.ParseBool(from.(string))
return reflect.ValueOf(val)
}
return fromrv.Convert(toType)
}
// ErrUnmarshalRequestFailed :
var ErrUnmarshalRequestFailed = errors.New("Unmarshal failed at rpc handler")
// CallMethodInternal :
func CallMethodInternal(receiver interface{}, context functionCallContext) (interface{}, error) {
methodTokens := strings.Split(context.Method, ".")
context.Method = methodTokens[len(methodTokens)-1]
for i := 0; i < len(methodTokens)-1; i++ {
token := methodTokens[i]
if strings.HasSuffix(token, "()") {
token = token[:len(token)-2]
}
fn := reflect.ValueOf(receiver).MethodByName(token)
// 이 fn은 인자를 받지 않고 하나만 리턴하는 함수여야 한다.
fnType := fn.Type()
if fnType.NumOut() != 1 || fnType.NumIn() != 0 {
return nil, errors.New(fmt.Sprint("method tokens are not correct :", token))
}
returnVals := fn.Call(nil)
if returnVals[0].IsNil() {
return nil, errors.New(fmt.Sprint("method token returns nil :", token))
}
receiver = returnVals[0].Interface()
}
fn := reflect.ValueOf(receiver).MethodByName(context.Method)
if fn.IsValid() {
fnType := fn.Type()
if fnType.NumOut() != 1 {
return nil, errors.New(fmt.Sprint("method should return only RPCReturnType :", context.Method))
}
if len(context.Args) != fnType.NumIn() {
return nil, errors.New(fmt.Sprint("argument is not matching :", context.Method))
}
var argv []reflect.Value
for i := 0; i < len(context.Args); i++ {
argv = append(argv, ConvertInterface(context.Args[i], fnType.In(i)))
}
returnVals := fn.Call(argv)
return returnVals[0].Interface(), nil
}
return nil, errors.New(fmt.Sprint("method is missing :", receiver, context.Method))
}
// CallMethod :
func CallMethod(receiver interface{}, context []byte) (retval RPCReturnType, err error) {
defer func() {
r := recover()
if r != nil && err == nil {
debug.PrintStack()
err = errors.New(fmt.Sprintf("%v", r))
}
}()
var meta functionCallContext
if err := json.Unmarshal(context, &meta); err != nil {
fmt.Println(string(context))
return nil, ErrUnmarshalRequestFailed
} else {
fmt.Printf("%+v\n", meta)
}
var v interface{}
v, err = CallMethodInternal(receiver, meta)
if err != nil {
return nil, err
}
// v는 RPCReturnType이어야 한다.
if cast, ok := v.(RPCReturnType); ok {
if cast.Error() != nil {
return nil, cast.Error()
}
return cast, nil
}
return nil, errors.New(fmt.Sprint("method should return only RPCReturnType", meta))
}