incorporated more configuration values into the timeout and panic recovery code

This commit is contained in:
2026-05-03 14:44:14 -06:00
parent e962c4d0c5
commit d3e89b886e
4 changed files with 124 additions and 92 deletions
+34 -20
View File
@@ -100,7 +100,10 @@ type AmConfig struct {
SessionExpire string `yaml:"sessionExpire"`
UserAgreementResource string `yaml:"userAgreementResource"`
PolicyResource string `yaml:"policyResource"`
FrameTemplate string `yaml:"frameTemplate"`
FooterTemplate string `yaml:"footerTemplate"`
TopMenuId string `yaml:"topMenuId"`
FixedMenuId string `yaml:"fixedMenuId"`
DefaultCommunityLogo string `yaml:"defaultCommunityLogo"`
DefaultUserPhoto string `yaml:"defaultUserPhoto"`
WelcomeTitle string `yaml:"welcomeTitle"`
@@ -148,6 +151,9 @@ type AmConfig struct {
Prioritize string `yaml:"prioritize"`
} `yaml:"countryList"`
VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"`
PanicRecovery struct {
StackDataSize string `yaml:"stackDataSize"`
} `yaml:"panicRecovery"`
} `yaml:"rendering"`
Resources struct {
ViewTemplateDir string `yaml:"viewTemplateDir"`
@@ -168,9 +174,11 @@ type AmConfig struct {
Tuning struct {
WorkerTasks int `yaml:"workerTasks"`
Timeouts struct {
HttpRead int `yaml:"httpRead"`
HttpWrite int `yaml:"httpWrite"`
HttpIdle int `yaml:"httpIdle"`
HttpRead int `yaml:"httpRead"`
HttpWrite int `yaml:"httpWrite"`
HttpIdle int `yaml:"httpIdle"`
PageExecute int `yaml:"pageExecute"`
PageRender int `yaml:"pageRender"`
} `yaml:"timeouts"`
Queues struct {
AuditWrites int `yaml:"auditWrites"`
@@ -207,22 +215,23 @@ func (c *AmConfig) ExPath(path string) string {
// AmConfigComputed is the configuration values which are "computed" based only on values in AmConfig and CommandLine.
type AmConfigComputed struct {
DebugMode bool // are we in debug mode?
LogLevel string // the logging level
Listen string // listen address
DatabaseDriver string // name of database driver
DatabaseHost string // hostname for database
DatabaseUser string // user name for database
DatabasePassword string // password for database
DatabaseName string // database name
MailHost string // SMTP host
MailPort int // SMTP port
MailTLS string // SMTP TLS setting
MailAuthType string // SMTP auth type
MailUser string // SMTP user name
MailPassword string // SMTP password
UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed?
DebugMode bool // are we in debug mode?
LogLevel string // the logging level
Listen string // listen address
DatabaseDriver string // name of database driver
DatabaseHost string // hostname for database
DatabaseUser string // user name for database
DatabasePassword string // password for database
DatabaseName string // database name
MailHost string // SMTP host
MailPort int // SMTP port
MailTLS string // SMTP TLS setting
MailAuthType string // SMTP auth type
MailUser string // SMTP user name
MailPassword string // SMTP password
PanicRecoveryStack int32 // stack size for panic recovery
UploadMaxSize int32 // maximum upload size in bytes
UploadNoCompress map[string]bool // which upload types are not compressed?
}
//go:embed default.yaml
@@ -403,7 +412,12 @@ func SetupConfig() {
GlobalComputedConfig.MailAuthType = util.IIF(CommandLine.MailAuthType != "", CommandLine.MailAuthType, GlobalConfig.Email.AuthType)
GlobalComputedConfig.MailUser = util.IIF(CommandLine.MailUser != "", CommandLine.MailUser, GlobalConfig.Email.User)
GlobalComputedConfig.MailPassword = util.IIF(CommandLine.MailPassword != "", CommandLine.MailPassword, GlobalConfig.Email.Password)
tmp, err := humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize)
tmp, err := humanize.ParseBytes(GlobalConfig.Rendering.PanicRecovery.StackDataSize)
if err != nil {
panic(err.Error())
}
GlobalComputedConfig.PanicRecoveryStack = int32(tmp)
tmp, err = humanize.ParseBytes(GlobalConfig.Posting.Uploads.MaxSize)
if err != nil {
panic(err.Error())
}
+7
View File
@@ -25,7 +25,10 @@ site:
sessionExpire: "3h"
userAgreementResource: "useragreement.html"
policyResource: "policy.html"
frameTemplate: "frame.jet"
footerTemplate: "footer.jet"
topMenuId: "top"
fixedMenuId: "fixed"
defaultCommunityLogo: "/img/builtin/default-community.jpg"
defaultUserPhoto: "/img/builtin/no-user.png"
welcomeTitle: "Welcome to Amsterdam"
@@ -72,6 +75,8 @@ rendering:
countryList:
prioritize: US
veniceCompatibleImageURLs: false
panicRecovery:
stackDataSize: "4 KiB"
resources:
viewTemplateDir: ""
dialogTemplateDir: ""
@@ -95,6 +100,8 @@ tuning:
httpRead: 30
httpWrite: 30
httpIdle: 120
pageExecute: 15
pageRender: 15
queues:
auditWrites: 16
contextRecycle: 16
+3 -1
View File
@@ -50,7 +50,9 @@ func setupEcho() *echo.Echo {
e.Renderer = &ui.TemplateRenderer{}
e.HTTPErrorHandler = AmErrorHandler
if !config.CommandLine.DebugPanic {
e.Use(middleware.RecoverWithConfig(middleware.DefaultRecoverConfig))
e.Use(middleware.RecoverWithConfig(middleware.RecoverConfig{
StackSize: int(config.GlobalComputedConfig.PanicRecoveryStack),
}))
} else {
log.Warn("WARNING: --debug-panic in effect - DO NOT use this in production!")
}
+80 -71
View File
@@ -1,6 +1,6 @@
/*
* Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
* Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -44,6 +44,51 @@ func (e *panicRecoveryErr) Unwrap() error {
return e.Err
}
// doFrameRender renders the outer frame template with an inner template.
func doFrameRender(ctxt *echo.Context, amctxt AmContext, statusCode int, innerPage string) error {
if amctxt.FrameTitle() == "" {
log.Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath())
amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>")
}
amctxt.VarMap().Set("__innerPage", innerPage)
menus := make([]*MenuDefinition, 2)
switch amctxt.LeftMenu() {
case "top":
menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId)
case "community":
comm := amctxt.CurrentCommunity()
if comm != nil {
md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm)
if err != nil {
return err
}
menus[0] = md
} else {
menus[0] = AmMenu(config.GlobalConfig.Site.TopMenuId)
}
default:
return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu())
}
menus[1] = AmMenu(config.GlobalConfig.Site.FixedMenuId)
amctxt.VarMap().Set("__leftMenus", menus)
ad, err := database.AmGetRandomAd(ctxt.Request().Context())
if err != nil {
ad = &database.Advert{
AdId: -1,
ImagePath: "",
PathStyle: -1,
Caption: nil,
LinkURL: nil,
}
}
amctxt.VarMap().Set("__bannerad", ad)
amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode)
if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil {
amctxt.VarMap().Set("__suppressLogin", true)
}
return ctxt.Render(statusCode, config.GlobalConfig.Site.FrameTemplate, amctxt)
}
/* AmSendPageData sends page data to the output based on the command string.
* Parameters:
* ctxt - The Echo context from the request.
@@ -65,20 +110,22 @@ func (e *panicRecoveryErr) Unwrap() error {
*/
func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data any) error {
// Enable panic recovery.
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
if !config.CommandLine.DebugPanic {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack)
length := runtime.Stack(stack, false)
log.Errorf("[Panic Recovery in SendData Phase] %s %s", tmperr.Error(), stack[:length])
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, 4<<10)
length := runtime.Stack(stack, false)
log.Errorf("[Panic Recovery in SendData Phase] %s %s", tmperr.Error(), stack[:length])
}
}()
}()
}
// Preprocess certain commands into different ones.
httprc := http.StatusOK
@@ -132,7 +179,7 @@ func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data a
// Process commands.
oldreq := ctxt.Request()
ctx, cancel := context.WithTimeout(oldreq.Context(), 15*time.Second)
ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageRender)*time.Second)
defer cancel()
ctxt.SetRequest(oldreq.WithContext(ctx))
defer ctxt.SetRequest(oldreq)
@@ -151,47 +198,7 @@ func AmSendPageData(ctxt *echo.Context, amctxt AmContext, command string, data a
case "template":
err = ctxt.Render(httprc, data.(string), amctxt)
case "framed":
if amctxt.FrameTitle() == "" {
log.Errorf("*** NO FRAME TITLE set for path %s", amctxt.URLPath())
amctxt.SetFrameTitle("<<< NO FRAME TITLE >>>")
}
amctxt.VarMap().Set("__innerPage", data)
menus := make([]*MenuDefinition, 2)
switch amctxt.LeftMenu() {
case "top":
menus[0] = AmMenu("top")
case "community":
comm := amctxt.CurrentCommunity()
if comm != nil {
md, err := AmBuildCommunityMenu(ctxt.Request().Context(), comm)
if err != nil {
return err
}
menus[0] = md
} else {
menus[0] = AmMenu("top")
}
default:
return fmt.Errorf("AmSendPageData(): unknown left menu context: %s", amctxt.LeftMenu())
}
menus[1] = AmMenu("fixed")
amctxt.VarMap().Set("__leftMenus", menus)
ad, err := database.AmGetRandomAd(ctxt.Request().Context())
if err != nil {
ad = &database.Advert{
AdId: -1,
ImagePath: "",
PathStyle: -1,
Caption: nil,
LinkURL: nil,
}
}
amctxt.VarMap().Set("__bannerad", ad)
amctxt.VarMap().Set("__debugMode", config.GlobalComputedConfig.DebugMode)
if tmp := amctxt.GetScratch("frame_suppressLogin"); tmp != nil {
amctxt.VarMap().Set("__suppressLogin", true)
}
err = ctxt.Render(httprc, "frame.jet", amctxt)
err = doFrameRender(ctxt, amctxt, httprc, data.(string))
default:
err = fmt.Errorf("AmSendPageData(): unknown rendering type: %s", command)
}
@@ -209,23 +216,25 @@ type AmPageFunc func(AmContext) (string, any)
// callWrappedPageFunc calls the specified page functon inside a wrapper that handles timeouts and panic recovery.
func callWrappedPageFunc(f AmPageFunc, ctxt *echo.Context, amctxt AmContext) (command string, arg any) {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
if !config.CommandLine.DebugPanic {
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, config.GlobalComputedConfig.PanicRecoveryStack)
length := runtime.Stack(stack, false)
arg = &panicRecoveryErr{Phase: "PageFunc", Err: tmperr, Stack: stack[:length]}
command = "error"
}
tmperr, ok := r.(error)
if !ok {
tmperr = fmt.Errorf("%v", r)
}
stack := make([]byte, 4<<10)
length := runtime.Stack(stack, false)
arg = &panicRecoveryErr{Phase: "PageFunc", Err: tmperr, Stack: stack[:length]}
command = "error"
}
}()
}()
}
oldreq := ctxt.Request()
ctx, cancel := context.WithTimeout(oldreq.Context(), 15*time.Second)
ctx, cancel := context.WithTimeout(oldreq.Context(), time.Duration(config.GlobalConfig.Tuning.Timeouts.PageExecute)*time.Second)
defer cancel()
ctxt.SetRequest(oldreq.WithContext(ctx))
defer ctxt.SetRequest(oldreq)