~bigbes/core-go

1371ba4bbb75e323ad6db3c4c5273feefc518f3d — Drew DeVault 4 years ago cb5aee1
Configure server to manage mail queue

This also sets up the recovery function for webhooks, so we get emailed
when we panic during a webhook query.
5 files changed, 31 insertions(+), 14 deletions(-)

M config/middleware.go
M email/worker.go
M server/email.go
M server/server.go
M webhooks/context.go
M config/middleware.go => config/middleware.go +7 -6
@@ 16,19 16,20 @@ type contextKey struct {
}

func Middleware(conf ini.File, service string) func(next http.Handler) http.Handler {
	svc := service
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := Context(r.Context(), conf)
			ctx = context.WithValue(ctx, serviceCtxKey, &svc)
			ctx := Context(r.Context(), conf, service)
			r = r.WithContext(ctx)
			next.ServeHTTP(w, r)
		})
	}
}

func Context(ctx context.Context, conf ini.File) context.Context {
	return context.WithValue(ctx, configCtxKey, conf)
func Context(ctx context.Context, conf ini.File, service string) context.Context {
	svc := service
	ctx = context.WithValue(ctx, configCtxKey, conf)
	ctx = context.WithValue(ctx, serviceCtxKey, &svc)
	return ctx
}

func ForContext(ctx context.Context) ini.File {


@@ 42,7 43,7 @@ func ForContext(ctx context.Context) ini.File {
func ServiceName(ctx context.Context) string {
	raw, ok := ctx.Value(serviceCtxKey).(*string)
	if !ok {
		panic(errors.New("Invalid config context"))
		panic(errors.New("Invalid service config context"))
	}
	return *raw
}

M email/worker.go => email/worker.go +7 -3
@@ 192,12 192,16 @@ func ForContext(ctx context.Context) *work.Queue {
	return q
}

// Returns a context which includes the given mail worker.
func Context(ctx context.Context, queue *work.Queue) context.Context {
	return context.WithValue(ctx, emailCtxKey, queue)
}

// Adds HTTP middleware to provide an email work queue to this context.
func Middleware(worker *work.Queue) func(next http.Handler) http.Handler {
func Middleware(queue *work.Queue) func(next http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := context.WithValue(r.Context(), emailCtxKey, worker)
			r = r.WithContext(ctx)
			r = r.WithContext(Context(r.Context(), queue))
			next.ServeHTTP(w, r)
		})
	}

M server/email.go => server/email.go +1 -1
@@ 19,7 19,7 @@ import (

// Provides a graphql.RecoverFunc which will print the stack trace, and if
// debug mode is not enabled, email it to the administrator.
func emailRecover(ctx context.Context, _origErr interface{}) error {
func EmailRecover(ctx context.Context, _origErr interface{}) error {
	var (
		ok      bool
		origErr error

M server/server.go => server/server.go +11 -3
@@ 30,6 30,7 @@ import (
	"git.sr.ht/~sircmpwn/core-go/auth"
	"git.sr.ht/~sircmpwn/core-go/config"
	"git.sr.ht/~sircmpwn/core-go/database"
	"git.sr.ht/~sircmpwn/core-go/email"
	"git.sr.ht/~sircmpwn/core-go/redis"
)



@@ 54,6 55,7 @@ type Server struct {
	router  chi.Router
	service string
	queues  []*work.Queue
	email   *work.Queue
}

// Creates a new common server context for a SourceHut GraphQL daemon.


@@ 95,14 97,14 @@ func (server *Server) WithSchema(

	srv := handler.GraphQL(schema,
		handler.ComplexityLimit(complexity),
		handler.RecoverFunc(emailRecover),
		handler.RecoverFunc(EmailRecover),
		handler.UploadMaxSize(1073741824)) // 1 GiB (TODO: configurable?)

	server.router.Handle("/query", srv)
	if config.Debug {
		server.router.Handle("/",
			playground.Handler("GraphQL playground", "/query"))
	}
	server.router.Handle("/query", srv)
	server.router.Handle("/query/metrics", promhttp.Handler())
	server.router.Get("/query/api-meta.json", func(w http.ResponseWriter, r *http.Request) {
		info := struct {


@@ 133,6 135,7 @@ type contextKey struct {
// - PostgresSQL connection pool
// - Redis connection
// - Authentication middleware
// - An email queue
// - Standard rigging: logging, x-real-ip, instrumentation, etc
func (server *Server) WithDefaultMiddleware() *Server {
	pgcs, ok := server.conf.Get(server.service, "connection-string")


@@ 169,6 172,8 @@ func (server *Server) WithDefaultMiddleware() *Server {
		timeout = 3 * time.Second
	}

	server.email = email.NewQueue()

	server.router.Use(func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()


@@ 180,6 185,7 @@ func (server *Server) WithDefaultMiddleware() *Server {
		})
	})
	server.router.Use(config.Middleware(server.conf, server.service))
	server.router.Use(email.Middleware(server.email))
	server.router.Use(database.Middleware(db))
	server.router.Use(redis.Middleware(rc))
	server.router.Use(middleware.RealIP)


@@ 194,6 200,7 @@ func (server *Server) WithDefaultMiddleware() *Server {
			next.ServeHTTP(w, r)
		})
	})
	server.WithQueues(server.email)
	return server
}



@@ 223,9 230,10 @@ func (server *Server) WithMiddleware(
// Add dowork task queues for this server to manage
func (server *Server) WithQueues(queues ...*work.Queue) *Server {
	ctx := context.Background()
	ctx = config.Context(ctx, server.conf)
	ctx = config.Context(ctx, server.conf, server.service)
	ctx = database.Context(ctx, server.db)
	ctx = redis.Context(ctx, server.redis)
	ctx = email.Context(ctx, server.email)

	server.queues = append(server.queues, queues...)
	for _, queue := range queues {

M webhooks/context.go => webhooks/context.go +5 -1
@@ 12,6 12,7 @@ import (
	"github.com/google/uuid"

	"git.sr.ht/~sircmpwn/core-go/auth"
	"git.sr.ht/~sircmpwn/core-go/server"
)

type contextKey struct {


@@ 63,6 64,7 @@ func (webhook *WebhookContext) Exec(ctx context.Context,
		return nil, err
	}

	// TODO: Set complexity limit
	exec := executor.New(schema)
	params := graphql.RawParams{
		Query: sub.Query,


@@ 76,8 78,10 @@ func (webhook *WebhookContext) Exec(ctx context.Context,
	if errors != nil {
		panic(errors)
	}
	ctx = graphql.WithOperationContext(ctx, rc)
	rc.RecoverFunc = server.EmailRecover

	var resp graphql.ResponseHandler
	ctx = graphql.WithOperationContext(ctx, rc)
	resp, ctx = exec.DispatchOperation(ctx, rc)
	payload, err := json.Marshal(resp(ctx))
	if err != nil {