package gocommon import ( "context" "encoding/json" "errors" "fmt" "io" "math" "net" "net/http" "net/url" "os" "os/signal" "reflect" "runtime" "strconv" "strings" "sync" "sync/atomic" "syscall" "time" "repositories.action2quare.com/ayo/gocommon/flagx" "repositories.action2quare.com/ayo/gocommon/logger" "github.com/pires/go-proxyproto" "go.mongodb.org/mongo-driver/bson/primitive" ) 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 RpcReturnTypeInterface interface { Value() any Error() error Serialize(http.ResponseWriter) error } type functionCallContext struct { Method string Args []interface{} } // Server : type Server struct { httpserver *http.Server interrupt chan os.Signal } var healthcheckcounter = int64(0) func healthCheckHandler(w http.ResponseWriter, r *http.Request) { defer func() { io.Copy(io.Discard, r.Body) r.Body.Close() }() // 한번이라도 들어오면 lb에 붙어있다는 뜻 if t := atomic.AddInt64(&healthcheckcounter, 1); t < 0 { logger.Println("healthCheckHandler return StatusServiceUnavailable :", t) w.WriteHeader(http.StatusServiceUnavailable) } } func welcomeHandler(w http.ResponseWriter, r *http.Request) { defer func() { io.Copy(io.Discard, r.Body) r.Body.Close() }() w.Write([]byte("welcome")) } var tls = flagx.String("tls", "", "") var portptr = flagx.Int("port", 80, "") // NewHTTPServer : func NewHTTPServerWithPort(serveMux *http.ServeMux, port int) *Server { if len(*tls) > 0 && port == 80 { port = 443 } addr := fmt.Sprintf(":%d", port) serveMux.HandleFunc(MakeHttpHandlerPattern("welcome"), welcomeHandler) serveMux.HandleFunc(MakeHttpHandlerPattern("lb_health_chceck"), healthCheckHandler) server := &Server{ httpserver: &http.Server{Addr: addr, Handler: serveMux}, interrupt: make(chan os.Signal, 1), } server.httpserver.SetKeepAlivesEnabled(true) signal.Notify(server.interrupt, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) go func() { c := <-server.interrupt logger.Println("interrupt!!!!!!!! :", c.String()) server.shutdown() }() return server } func NewHTTPServer(serveMux *http.ServeMux) *Server { // 시작시 자동으로 enable됨 if len(*tls) > 0 && *portptr == 80 { *portptr = 443 } return NewHTTPServerWithPort(serveMux, *portptr) } // Shutdown : func (server *Server) shutdown() { logger.Println("server.shutdown()") signal.Stop(server.interrupt) if t := atomic.LoadInt64(&healthcheckcounter); t > 0 { logger.Println("http server shutdown. healthcheckcounter :", t) atomic.StoreInt64(&healthcheckcounter, math.MinInt64) for cnt := 0; cnt < 100; { next := atomic.LoadInt64(&healthcheckcounter) if next == t { cnt++ } else { t = next cnt = 0 } time.Sleep(100 * time.Millisecond) } logger.Println("http server shutdown. healthcheck completed") } else { logger.Println("http server shutdown. no lb") } if server.httpserver != nil { server.httpserver.Shutdown(context.Background()) server.httpserver = nil } } // Start : func (server *Server) Start() error { if server.httpserver != nil { ln, r := net.Listen("tcp", server.httpserver.Addr) if r != nil { return r } proxyListener := &proxyproto.Listener{ Listener: ln, ReadHeaderTimeout: 10 * time.Second, } defer proxyListener.Close() var err error if len(*tls) > 0 { crtfile := *tls + ".crt" keyfile := *tls + ".key" logger.Println("tls enabled :", crtfile, keyfile) err = server.httpserver.ServeTLS(proxyListener, crtfile, keyfile) } else { err = server.httpserver.Serve(proxyListener) } if err != nil { if errors.Is(err, http.ErrServerClosed) { logger.Println("server.httpserver is closed normally") return nil } logger.Error("server.httpserver.Serve err :", err) return err } } return nil } // ConvertInterface : func ConvertInterface(from interface{}, toType reflect.Type) reflect.Value { fromrv := reflect.ValueOf(from) fromtype := reflect.TypeOf(from) if fromtype == toType { return fromrv } switch fromtype.Kind() { case reflect.Map: frommap := from.(map[string]interface{}) if frommap == nil { // map[string]interface{}가 아니고 뭐란말인가 return reflect.Zero(toType) } needAddr := false if toType.Kind() == reflect.Pointer { toType = toType.Elem() needAddr = true } out := reflect.New(toType) switch toType.Kind() { case reflect.Struct: for i := 0; i < toType.NumField(); i++ { field := toType.FieldByIndex([]int{i}) if field.Anonymous { fieldval := out.Elem().FieldByIndex([]int{i}) fieldval.Set(ConvertInterface(from, field.Type)) } else if v, ok := frommap[strings.ToLower(field.Name)]; ok { fieldval := out.Elem().FieldByIndex([]int{i}) fieldval.Set(ConvertInterface(v, fieldval.Type())) } } case reflect.Map: out.Elem().Set(reflect.MakeMap(toType)) if toType.Key().Kind() != reflect.String { for k, v := range frommap { tok := reflect.ValueOf(k).Convert(toType.Key()) tov := ConvertInterface(v, toType.Elem()) out.Elem().SetMapIndex(tok, tov) } } else { for k, v := range frommap { tov := ConvertInterface(v, toType.Elem()) out.Elem().SetMapIndex(reflect.ValueOf(k), tov) } } default: logger.Println("ConvertInterface failed : map ->", toType.Kind().String()) } if needAddr { return out } return out.Elem() case reflect.Slice: 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 case 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") var ErrWithStatusCode = errors.New("custom error with status code") var ErrSecondReturnShouldBeErrorType = errors.New("second return type should be error") type methodCacheType struct { sync.Mutex toIndex map[reflect.Type]map[string]int } var methodCache = methodCacheType{ toIndex: make(map[reflect.Type]map[string]int), } func (mc *methodCacheType) method(receiver any, method string) (reflect.Value, bool) { mc.Lock() defer mc.Unlock() t := reflect.TypeOf(receiver) table, ok := mc.toIndex[t] if !ok { table = make(map[string]int) mc.toIndex[t] = table } idx, ok := table[method] if !ok { m, ok := t.MethodByName(method) if !ok { return reflect.Value{}, false } table[method] = m.Index idx = m.Index } return reflect.ValueOf(receiver).Method(idx), true } // CallMethodInternal : func CallMethodInternal(receiver interface{}, context functionCallContext) (result RpcReturnTypeInterface, err error) { fn, ok := methodCache.method(receiver, context.Method) if !ok { return nil, errors.New(fmt.Sprint("method is missing :", receiver, context.Method)) } if len(context.Args) != fn.Type().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], fn.Type().In(i))) } returnVals := fn.Call(argv) if len(returnVals) == 0 { return nil, ErrSecondReturnShouldBeErrorType } result = nil err = nil if result, ok = returnVals[0].Interface().(RpcReturnTypeInterface); !ok { return nil, ErrSecondReturnShouldBeErrorType } if len(returnVals) > 1 { if err, ok = returnVals[1].Interface().(error); !ok { return nil, ErrSecondReturnShouldBeErrorType } } return } // CallMethod : func CallMethod(receiver interface{}, context []byte) (RpcReturnTypeInterface, error) { defer func() { r := recover() if r != nil { logger.Error(r) } }() var meta functionCallContext if err := json.Unmarshal(context, &meta); err != nil { return nil, ErrUnmarshalRequestFailed } return CallMethodInternal(receiver, meta) } func ReadIntegerFormValue(r url.Values, key string) (int64, bool) { strval := r.Get(key) if len(strval) == 0 { return 0, false } temp, err := strconv.ParseInt(strval, 10, 0) if err != nil { logger.Error("common.ReadFloatFormValue failed :", key, err) return 0, false } return temp, true } func ReadFloatFormValue(r url.Values, key string) (float64, bool) { var inst float64 strval := r.Get(key) if len(strval) == 0 { return inst, false } temp, err := strconv.ParseFloat(strval, 64) if err != nil { logger.Error("common.ReadFloatFormValue failed :", key, err) return inst, false } return temp, true } func ReadObjectIDFormValue(r url.Values, key string) (primitive.ObjectID, bool) { strval := r.Get(key) if len(strval) == 0 { return primitive.NilObjectID, false } id, err := primitive.ObjectIDFromHex(strval) if err != nil { logger.Error("common.ReadObjectIDFormValue failed :", key, err) return primitive.NilObjectID, false } return id, true } func ReadBoolFormValue(r url.Values, key string) (bool, bool) { strval := r.Get(key) if len(strval) == 0 { return false, false } ret, err := strconv.ParseBool(strval) if err != nil { logger.Error("common.ReadBoolFormValue failed :", key, err) return false, false } return ret, true } func ReadStringFormValue(r url.Values, key string) (string, bool) { strval := r.Get(key) return strval, len(strval) > 0 } func DotStringToTimestamp(tv string) primitive.Timestamp { if len(tv) == 0 { return primitive.Timestamp{T: 0, I: 0} } ti := strings.Split(tv, ".") t, _ := strconv.ParseUint(ti[0], 10, 0) if len(ti) > 1 { i, _ := strconv.ParseUint(ti[1], 10, 0) return primitive.Timestamp{T: uint32(t), I: uint32(i)} } return primitive.Timestamp{T: uint32(t), I: 0} } type RpcReturnSimple struct { value any } func (rt *RpcReturnSimple) Value() any { return rt.value } func (rt *RpcReturnSimple) Error() error { return nil } func (rt *RpcReturnSimple) Serialize(w http.ResponseWriter) error { err := SerializeInterface(w, rt.value) if err == nil { w.Write([]byte{0, 0}) } return err } type RpcReturnError struct { err error code int h map[string]any } func (rt *RpcReturnError) Value() any { return nil } var errDefaultError = errors.New("unknown error") func (rt *RpcReturnError) Error() error { if rt.err != nil { return rt.err } if rt.code != 0 { if rt.h == nil { return fmt.Errorf("http status code %d error", rt.code) } return fmt.Errorf("http status code %d error with header %v", rt.code, rt.h) } return errDefaultError } func (rt *RpcReturnError) Serialize(w http.ResponseWriter) error { if rt.h != nil { bt, _ := json.Marshal(rt.h) w.Header().Add("As-X-Err", string(bt)) } w.WriteHeader(rt.code) if rt.err != nil { logger.Error(rt.err) } if rt.code >= http.StatusInternalServerError { logger.Println("rpc return error :", rt.code, rt.h) } return nil } func (rt *RpcReturnError) WithCode(code int) *RpcReturnError { rt.code = code return rt } func (rt *RpcReturnError) WithError(err error) *RpcReturnError { rt.err = err return rt } func (rt *RpcReturnError) WithHeader(k string, v any) *RpcReturnError { if rt.h == nil { rt.h = make(map[string]any) } rt.h[k] = v return rt } func (rt *RpcReturnError) WithHeaders(h map[string]any) *RpcReturnError { if rt.h == nil { rt.h = h } else { for k, v := range h { rt.h[k] = v } } return rt } // MakeRPCReturn : func MakeRPCReturn(value interface{}) *RpcReturnSimple { return &RpcReturnSimple{ value: value, } } func MakeRPCError() *RpcReturnError { pc, _, _, ok := runtime.Caller(1) if ok { frames := runtime.CallersFrames([]uintptr{pc}) frame, _ := frames.Next() logger.Printf("rpc error. func=%s, file=%s, line=%d", frame.Function, frame.File, frame.Line) } return &RpcReturnError{ err: nil, code: http.StatusInternalServerError, h: nil, } } type indirectBody struct { inner io.ReadCloser dump []byte closer func() } func (ib *indirectBody) Read(p []byte) (n int, err error) { n, err = ib.inner.Read(p) if n > 0 { ib.dump = append(ib.dump, p...) } return } func (ib *indirectBody) Close() error { if ib.closer != nil { ib.closer() ib.closer = nil } return ib.inner.Close() } func MakeHttpRequestForLogging(r *http.Request) *http.Request { ib := &indirectBody{ inner: r.Body, } closer := func() { var uv url.Values if r.Form != nil { uv = r.Form } else { uv = r.URL.Query() } logger.Println("request :") logger.Println(" header :", r.Header) logger.Println(" values :", uv) logger.Println(" body :", string(ib.dump)) } ib.closer = closer r.Body = ib return r }