You've already forked golang-saas-starter-kit
mirror of
https://github.com/raseels-repos/golang-saas-starter-kit.git
synced 2025-07-15 01:34:32 +02:00
Scale db middleware
Ensure the database is active and has not been auto paused by RDS. Resume database for signup and login pages.
This commit is contained in:
@ -104,7 +104,7 @@ func main() {
|
||||
TemplateDir string `default:"./templates" envconfig:"TEMPLATE_DIR"`
|
||||
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
ScaleToZero time.Duration `envconfig:"SCALE_TO_ZERO"`
|
||||
ScaleToZero time.Duration `envconfig:"SCALE_TO_ZERO"`
|
||||
}
|
||||
Project struct {
|
||||
Name string `default:"" envconfig:"PROJECT_NAME"`
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_account/invite"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/user_auth"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/ikeikeikeike/go-sitemap-generator/v2/stm"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/contrib/go-redis/redis"
|
||||
@ -40,6 +41,7 @@ type AppContext struct {
|
||||
Log *log.Logger
|
||||
Env webcontext.Env
|
||||
MasterDB *sqlx.DB
|
||||
MasterDbHost string
|
||||
Redis *redis.Client
|
||||
UserRepo *user.Repository
|
||||
UserAccountRepo *user_account.Repository
|
||||
@ -57,6 +59,7 @@ type AppContext struct {
|
||||
ProjectRoute project_route.ProjectRoute
|
||||
PreAppMiddleware []web.Middleware
|
||||
PostAppMiddleware []web.Middleware
|
||||
AwsSession *session.Session
|
||||
}
|
||||
|
||||
// API returns a handler for a set of routes.
|
||||
@ -81,6 +84,24 @@ func APP(shutdown chan os.Signal, appCtx *AppContext) http.Handler {
|
||||
// Construct the web.App which holds all routes as well as common Middleware.
|
||||
app := web.NewApp(shutdown, appCtx.Log, appCtx.Env, middlewares...)
|
||||
|
||||
// Register serverless endpoint. This route is not authenticated.
|
||||
serverless := Serverless{
|
||||
MasterDB: appCtx.MasterDB,
|
||||
MasterDbHost: appCtx.MasterDbHost,
|
||||
AwsSession: appCtx.AwsSession,
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("GET", "/serverless/pending", serverless.Pending)
|
||||
|
||||
// waitDbMid ensures the database is active before allowing the user to access the requested URI.
|
||||
waitDbMid := mid.WaitForDbResumed(mid.WaitForDbResumedConfig{
|
||||
// Database handle to be used to ensure its online.
|
||||
DB: appCtx.MasterDB,
|
||||
|
||||
// WaitHandler defines the handler to render for the user to when the database is being resumed.
|
||||
WaitHandler: serverless.Pending,
|
||||
})
|
||||
|
||||
// Build a sitemap.
|
||||
sm := stm.NewSitemap(1)
|
||||
sm.SetVerbose(false)
|
||||
@ -146,7 +167,7 @@ func APP(shutdown chan os.Signal, appCtx *AppContext) http.Handler {
|
||||
Renderer: appCtx.Renderer,
|
||||
}
|
||||
app.Handle("POST", "/user/login", u.Login)
|
||||
app.Handle("GET", "/user/login", u.Login)
|
||||
app.Handle("GET", "/user/login", u.Login, waitDbMid)
|
||||
app.Handle("GET", "/user/logout", u.Logout)
|
||||
app.Handle("POST", "/user/reset-password/:hash", u.ResetConfirm)
|
||||
app.Handle("GET", "/user/reset-password/:hash", u.ResetConfirm)
|
||||
@ -188,7 +209,7 @@ func APP(shutdown chan os.Signal, appCtx *AppContext) http.Handler {
|
||||
}
|
||||
// This route is not authenticated
|
||||
app.Handle("POST", "/signup", s.Step1)
|
||||
app.Handle("GET", "/signup", s.Step1)
|
||||
app.Handle("GET", "/signup", s.Step1, waitDbMid)
|
||||
|
||||
// Register example endpoints.
|
||||
ex := Examples{
|
||||
@ -224,6 +245,15 @@ func APP(shutdown chan os.Signal, appCtx *AppContext) http.Handler {
|
||||
app.Handle("GET", "/robots.txt", r.RobotTxt)
|
||||
app.Handle("GET", "/sitemap.xml", r.SitemapXml)
|
||||
|
||||
// Register health check endpoint. This route is not authenticated.
|
||||
check := Check{
|
||||
MasterDB: appCtx.MasterDB,
|
||||
Redis: appCtx.Redis,
|
||||
}
|
||||
|
||||
app.Handle("GET", "/v1/health", check.Health)
|
||||
app.Handle("GET", "/ping", check.Ping)
|
||||
|
||||
// Add sitemap entries for Root.
|
||||
smLocAddModified(stm.URL{{"loc", "/"}, {"changefreq", "weekly"}, {"mobile", true}, {"priority", 0.9}}, "site-index.gohtml")
|
||||
smLocAddModified(stm.URL{{"loc", "/pricing"}, {"changefreq", "monthly"}, {"mobile", true}, {"priority", 0.8}}, "site-pricing.gohtml")
|
||||
@ -232,14 +262,6 @@ func APP(shutdown chan os.Signal, appCtx *AppContext) http.Handler {
|
||||
smLocAddModified(stm.URL{{"loc", "/legal/privacy"}, {"changefreq", "monthly"}, {"mobile", true}, {"priority", 0.5}}, "legal-privacy.gohtml")
|
||||
smLocAddModified(stm.URL{{"loc", "/legal/terms"}, {"changefreq", "monthly"}, {"mobile", true}, {"priority", 0.5}}, "legal-terms.gohtml")
|
||||
|
||||
// Register health check endpoint. This route is not authenticated.
|
||||
check := Check{
|
||||
MasterDB: appCtx.MasterDB,
|
||||
Redis: appCtx.Redis,
|
||||
}
|
||||
app.Handle("GET", "/v1/health", check.Health)
|
||||
app.Handle("GET", "/ping", check.Ping)
|
||||
|
||||
// Handle static files/pages. Render a custom 404 page when file not found.
|
||||
static := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
err := web.StaticHandler(ctx, w, r, params, appCtx.StaticDir, "")
|
||||
|
104
cmd/web-app/handlers/serverless.go
Normal file
104
cmd/web-app/handlers/serverless.go
Normal file
@ -0,0 +1,104 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/mid"
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/rds"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Serverless provides support for ensuring serverless resources are available for the user. .
|
||||
type Serverless struct {
|
||||
Renderer web.Renderer
|
||||
MasterDB *sqlx.DB
|
||||
MasterDbHost string
|
||||
AwsSession *session.Session
|
||||
}
|
||||
|
||||
// WaitDb validates the the database is resumed and ready to accept requests.
|
||||
func (h *Serverless) Pending(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
|
||||
var redirectUri string
|
||||
if v, ok := ctx.Value(mid.ServerlessKey).(error); ok && v != nil {
|
||||
redirectUri = r.RequestURI
|
||||
} else {
|
||||
redirectUri = r.URL.Query().Get("redirect")
|
||||
}
|
||||
|
||||
if redirectUri == "" {
|
||||
redirectUri = "/"
|
||||
}
|
||||
|
||||
f := func() (bool, error) {
|
||||
svc := rds.New(h.AwsSession)
|
||||
|
||||
res, err := svc.DescribeDBClusters(&rds.DescribeDBClustersInput{})
|
||||
if err != nil {
|
||||
return false, errors.WithMessage(err, "Failed to list AWS db clusters.")
|
||||
}
|
||||
|
||||
var targetCluster *rds.DBCluster
|
||||
for _, c := range res.DBClusters {
|
||||
if c.Endpoint == nil || *c.Endpoint != h.MasterDbHost {
|
||||
continue
|
||||
}
|
||||
|
||||
targetCluster = c
|
||||
}
|
||||
|
||||
if targetCluster == nil {
|
||||
return false, errors.New("Failed to find database cluster.")
|
||||
} else if targetCluster.ScalingConfigurationInfo == nil || targetCluster.ScalingConfigurationInfo.MinCapacity == nil {
|
||||
return false, errors.New("Database cluster has now scaling configuration.")
|
||||
}
|
||||
|
||||
if targetCluster.Capacity != nil && *targetCluster.Capacity > 0 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
_, err = svc.ModifyCurrentDBClusterCapacity(&rds.ModifyCurrentDBClusterCapacityInput{
|
||||
DBClusterIdentifier: targetCluster.DBClusterIdentifier,
|
||||
Capacity: targetCluster.ScalingConfigurationInfo.MinCapacity,
|
||||
SecondsBeforeTimeout: aws.Int64(10),
|
||||
TimeoutAction: aws.String("ForceApplyCapacityChange"),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
end, err := f()
|
||||
if err != nil {
|
||||
return web.RenderError(ctx, w, r, err, h.Renderer, TmplLayoutBase, TmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
|
||||
}
|
||||
|
||||
if web.RequestIsJson(r) {
|
||||
data := map[string]interface{}{
|
||||
"redirectUri": redirectUri,
|
||||
"statusCode": http.StatusServiceUnavailable,
|
||||
}
|
||||
if end {
|
||||
data["statusCode"] = http.StatusOK
|
||||
}
|
||||
return web.RespondJson(ctx, w, data, http.StatusOK)
|
||||
}
|
||||
|
||||
if end {
|
||||
// Redirect the user to their requested page.
|
||||
return web.Redirect(ctx, w, r, redirectUri, http.StatusFound)
|
||||
}
|
||||
|
||||
data := map[string]interface{}{
|
||||
"statusUrl": "/serverless/pending?redirect=" + url.QueryEscape(redirectUri),
|
||||
}
|
||||
return h.Renderer.Render(ctx, w, r, TmplLayoutBase, "serverless-db.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
|
||||
}
|
@ -107,7 +107,7 @@ func main() {
|
||||
SessionName string `default:"" envconfig:"SESSION_NAME"`
|
||||
DebugHost string `default:"0.0.0.0:4000" envconfig:"DEBUG_HOST"`
|
||||
ShutdownTimeout time.Duration `default:"5s" envconfig:"SHUTDOWN_TIMEOUT"`
|
||||
ScaleToZero time.Duration `envconfig:"SCALE_TO_ZERO"`
|
||||
ScaleToZero time.Duration `envconfig:"SCALE_TO_ZERO"`
|
||||
}
|
||||
Project struct {
|
||||
Name string `default:"" envconfig:"PROJECT_NAME"`
|
||||
@ -456,6 +456,7 @@ func main() {
|
||||
Log: log,
|
||||
Env: cfg.Env,
|
||||
MasterDB: masterDb,
|
||||
MasterDbHost: cfg.DB.Host,
|
||||
Redis: redisClient,
|
||||
TemplateDir: cfg.Service.TemplateDir,
|
||||
StaticDir: cfg.Service.StaticFiles.Dir,
|
||||
@ -470,6 +471,7 @@ func main() {
|
||||
InviteRepo: inviteRepo,
|
||||
ProjectRepo: prjRepo,
|
||||
Authenticator: authenticator,
|
||||
AwsSession: awsSession,
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
51
cmd/web-app/templates/content/serverless-db.gohtml
Normal file
51
cmd/web-app/templates/content/serverless-db.gohtml
Normal file
@ -0,0 +1,51 @@
|
||||
{{define "title"}}Service Scaling{{end}}
|
||||
{{define "description"}}Service is scaling.{{end}}
|
||||
{{define "style"}}
|
||||
|
||||
{{end}}
|
||||
{{ define "partials/app-wrapper" }}
|
||||
<div class="container" id="page-content">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-xl-10 col-lg-12 col-md-9">
|
||||
<div class="card o-hidden border-0 shadow-lg my-5">
|
||||
<div class="card-body p-0">
|
||||
<!-- Nested Row within Card Body -->
|
||||
<div class="row">
|
||||
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
|
||||
<div class="col-lg-6">
|
||||
<div class="p-5">
|
||||
{{ template "app-flashes" . }}
|
||||
|
||||
<div class="text-center" style="margin-bottom: 250px; ">
|
||||
<h1 class="h4 text-gray-900 mb-4">The service is scaling up!</h1>
|
||||
<p>Please wait a moment, you will be redirected to your request page when operation complete.</p>
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="sr-only">Scaling...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{define "js"}}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$(document).find('body').addClass('bg-gradient-primary');
|
||||
|
||||
$.ajax({
|
||||
contentType: "application/json",
|
||||
url: '{{ $.statusUrl }}',
|
||||
dataType: "json"
|
||||
}).done(function(data) {
|
||||
if (data.statusCode == 200) {
|
||||
window.location = data.redirectUri;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
79
internal/mid/serverless.go
Normal file
79
internal/mid/serverless.go
Normal file
@ -0,0 +1,79 @@
|
||||
package mid
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
|
||||
)
|
||||
|
||||
// ctxServerlessKey represents the type of value for the context key.
|
||||
type ctxServerlessKey int
|
||||
|
||||
// Key is used to store/retrieve a Serverless value from a context.Context.
|
||||
const ServerlessKey ctxServerlessKey = 1
|
||||
|
||||
type (
|
||||
// WaitForDbResumedConfig defines the config for WaitForDbResumed middleware.
|
||||
WaitForDbResumedConfig struct {
|
||||
RedirectConfig
|
||||
|
||||
// Database handle to be used to ensure its online.
|
||||
DB *sqlx.DB
|
||||
|
||||
// WaitHandler defines the handler to render for the user to when the database is being resumed.
|
||||
WaitHandler web.Handler
|
||||
}
|
||||
)
|
||||
|
||||
// WaitForDbResumed returns an middleware with for ensuring an serverless database is resumed.
|
||||
func WaitForDbResumed(config WaitForDbResumedConfig) web.Middleware {
|
||||
|
||||
if config.Skipper == nil {
|
||||
config.Skipper = DefaultSkipper
|
||||
}
|
||||
if config.Code == 0 {
|
||||
config.Code = DefaultRedirectConfig.Code
|
||||
}
|
||||
|
||||
verifyDb := func() error {
|
||||
// When the database is paused, Postgres will return the error, "Canceling statement due to user request"
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
|
||||
defer cancel()
|
||||
|
||||
_, err := config.DB.ExecContext(ctx, "SELECT NULL")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// This is the actual middleware function to be executed.
|
||||
f := func(after web.Handler) web.Handler {
|
||||
|
||||
h := func(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
|
||||
span, ctx := tracer.StartSpanFromContext(ctx, "internal.mid.serverless")
|
||||
defer span.Finish()
|
||||
|
||||
if config.Skipper(ctx, w, r, params) {
|
||||
return after(ctx, w, r, params)
|
||||
}
|
||||
|
||||
if err := verifyDb(); err != nil {
|
||||
ctx = context.WithValue(ctx, ServerlessKey, err)
|
||||
return config.WaitHandler(ctx, w, r, params)
|
||||
}
|
||||
|
||||
return after(ctx, w, r, params)
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
@ -413,7 +413,7 @@ func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, re
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
if st, ok := err.(stackTracer); !ok ||st == nil || st.StackTrace() == nil {
|
||||
if st, ok := err.(stackTracer); !ok || st == nil || st.StackTrace() == nil {
|
||||
err = errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user