context transactions removed and make Context.Domain customizable as requested

This commit is contained in:
Gerasimos (Makis) Maropoulos 2022-06-05 06:15:10 +03:00
parent c6911851f1
commit d8af2a1e14
No known key found for this signature in database
GPG Key ID: 3595CBE7F3B4082E
8 changed files with 28 additions and 313 deletions

View File

@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements
- Make `Context.Domain()` customizable by letting developers to set the custom `Context.GetDomain` package-level function.
- Remove Request Context-based Transaction feature as its usage can be replaced with just the Iris Context (as of go1.7+) and better [project](_examples/project) structure.
- Fix [#1882](https://github.com/kataras/iris/issues/1882)
- Fix [#1877](https://github.com/kataras/iris/issues/1877)
- Fix [#1876](https://github.com/kataras/iris/issues/1876)

View File

@ -200,7 +200,6 @@
* [Protocol Buffers](response-writer/protobuf/main.go)
* [HTTP/2 Server Push](response-writer/http2push/main.go)
* [Stream Writer](response-writer/stream-writer/main.go)
* [Transactions](response-writer/transactions/main.go)
* [Server-Sent Events](response-writer/sse/main.go)
* [SSE 3rd-party (r3labs/sse)](response-writer/sse-third-party/main.go)
* [SSE 3rd-party (alexandrevicenzi/go-sse)](response-writer/sse-third-party-2/main.go)

View File

@ -1,54 +0,0 @@
package main
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
)
func main() {
app := iris.New()
// subdomains works with all available routers, like other features too.
app.Get("/", func(ctx iris.Context) {
ctx.BeginTransaction(func(t *context.Transaction) {
// OPTIONAl STEP: , if true then the next transictions will not be executed if this transiction fails
// t.SetScope(context.RequestTransactionScope)
// OPTIONAL STEP:
// create a new custom type of error here to keep track of the status code and reason message
err := context.NewTransactionErrResult()
// we should use t.Context if we want to rollback on any errors lives inside this function clojure.
t.Context().Text("Blablabla this should not be sent to the client because we will fill the err with a message and status")
// virtualize a fake error here, for the sake of the example
fail := true
if fail {
err.StatusCode = iris.StatusInternalServerError
// NOTE: if empty reason then the default or the custom http error will be fired (like ctx.FireStatusCode)
err.Reason = "Error: Virtual failure!!"
}
// OPTIONAl STEP:
// but useful if we want to post back an error message to the client if the transaction failed.
// if the reason is empty then the transaction completed successfully,
// otherwise we rollback the whole response writer's body,
// headers and cookies, status code and everything lives inside this transaction
t.Complete(err)
})
ctx.BeginTransaction(func(t *context.Transaction) {
t.Context().HTML("<h1>This will sent at all cases because it lives on different transaction and it doesn't fails</h1>")
// * if we don't have any 'throw error' logic then no need of scope.Complete()
})
// OPTIONALLY, depends on the usage:
// at any case, what ever happens inside the context's transactions send this to the client
ctx.HTML("<h1>Let's add a second html message to the response, " +
"if the transaction was failed and it was request scoped then this message would " +
"not been shown. But it has a transient scope(default) so, it is visible as expected!</h1>")
})
app.Listen(":8080")
}

View File

@ -1001,7 +1001,8 @@ func (ctx *Context) Host() string {
}
// GetDomain resolves and returns the server's domain.
func GetDomain(hostport string) string {
// To customize its behavior, developers can modify this package-level function at initialization.
var GetDomain = func(hostport string) string {
host := hostport
if tmp, _, err := net.SplitHostPort(hostport); err == nil {
host = tmp
@ -1459,7 +1460,7 @@ func (ctx *Context) GetContentLength() int64 {
// StatusCode sets the status code header to the response.
// Look .GetStatusCode & .FireStatusCode too.
//
// Remember, the last one before .Write matters except recorder and transactions.
// Note that you must set status code before write response body (except when recorder is used).
func (ctx *Context) StatusCode(statusCode int) {
ctx.writer.WriteHeader(statusCode)
}
@ -5656,7 +5657,7 @@ func (ctx *Context) MaxAge() int64 {
}
// +------------------------------------------------------------+
// | Advanced: Response Recorder and Transactions |
// | Advanced: Response Recorder |
// +------------------------------------------------------------+
// Record transforms the context's basic and direct responseWriter to a *ResponseRecorder
@ -5691,72 +5692,6 @@ func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
return rr, ok
}
// ErrTransactionInterrupt can be used to manually force-complete a Context's transaction
// and log(warn) the wrapped error's message.
// Usage: `... return fmt.Errorf("my custom error message: %w", context.ErrTransactionInterrupt)`.
var ErrTransactionInterrupt = errors.New("transaction interrupted")
// BeginTransaction starts a scoped transaction.
//
// Can't say a lot here because it will take more than 200 lines to write about.
// You can search third-party articles or books on how Business Transaction works (it's quite simple, especially here).
//
// Note that this is unique and new
// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...)
// it's not covers all paths,
// such as databases, this should be managed by the libraries you use to make your database connection,
// this transaction scope is only for context's response.
// Transactions have their own middleware ecosystem also.
//
// See https://github.com/kataras/iris/tree/master/_examples/ for more
func (ctx *Context) BeginTransaction(pipe func(t *Transaction)) {
// do NOT begin a transaction when the previous transaction has been failed
// and it was requested scoped or SkipTransactions called manually.
if ctx.TransactionsSkipped() {
return
}
// start recording in order to be able to control the full response writer
ctx.Record()
t := newTransaction(ctx) // it calls this *context, so the overriding with a new pool's New of context.Context wil not work here.
defer func() {
if err := recover(); err != nil {
ctx.app.Logger().Warn(fmt.Errorf("recovery from panic: %w", ErrTransactionInterrupt))
// complete (again or not , doesn't matters) the scope without loud
t.Complete(nil)
// we continue as normal, no need to return here*
}
// write the temp contents to the original writer
t.Context().ResponseWriter().CopyTo(ctx.writer)
// give back to the transaction the original writer (SetBeforeFlush works this way and only this way)
// this is tricky but nessecery if we want ctx.FireStatusCode to work inside transactions
t.Context().ResetResponseWriter(ctx.writer)
}()
// run the worker with its context clone inside.
pipe(t)
}
// skipTransactionsContextKey set this to any value to stop executing next transactions
// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions()
const skipTransactionsContextKey = "iris.transactions.skip"
// SkipTransactions if called then skip the rest of the transactions
// or all of them if called before the first transaction
func (ctx *Context) SkipTransactions() {
ctx.values.Set(skipTransactionsContextKey, 1)
}
// TransactionsSkipped returns true if the transactions skipped or canceled at all.
func (ctx *Context) TransactionsSkipped() bool {
if n, err := ctx.values.GetInt(skipTransactionsContextKey); err == nil && n == 1 {
return true
}
return false
}
// Exec calls the framewrok's ServeHTTPC
// based on this context but with a changed method and path
// like it was requested by the user, but it is not.

View File

@ -29,17 +29,14 @@ func releaseResponseRecorder(w *ResponseRecorder) {
rrpool.Put(w)
}
// A ResponseRecorder is used mostly by context's transactions
// in order to record and change if needed the body, status code and headers.
// A ResponseRecorder is used mostly for testing
// in order to record and modify, if necessary, the body and status code and headers.
//
// Developers are not limited to manually ask to record a response.
// To turn on the recorder from a Handler,
// rec := context.Recorder()
// See `context.Recorder`` method too.
type ResponseRecorder struct {
ResponseWriter
// keep track of the body in order to be
// resetable and useful inside custom transactions
// keep track of the body written.
chunks []byte
// the saved headers
headers http.Header

View File

@ -1,179 +0,0 @@
package context
// TransactionErrResult could be named also something like 'MaybeError',
// it is useful to send it on transaction.Complete in order to execute a custom error mesasge to the user.
//
// in simple words it's just a 'traveler message' between the transaction and its scope.
// it is totally optional
type TransactionErrResult struct {
StatusCode int
// if reason is empty then the already relative registered (custom or not)
// error will be executed if the scope allows that.
Reason string
ContentType string
}
// Error returns the reason given by the user or an empty string
func (err TransactionErrResult) Error() string {
return err.Reason
}
// IsFailure returns true if this is an actual error
func (err TransactionErrResult) IsFailure() bool {
return StatusCodeNotSuccessful(err.StatusCode)
}
// NewTransactionErrResult returns a new transaction result with the given error message,
// it can be empty too, but if not then the transaction's scope is decided what to do with that
func NewTransactionErrResult() TransactionErrResult {
return TransactionErrResult{}
}
// TransactionScope is the manager of the transaction's response, can be resseted and skipped
// from its parent context or execute an error or skip other transactions
type TransactionScope interface {
// EndTransaction returns if can continue to the next transactions or not (false)
// called after Complete, empty or not empty error
EndTransaction(maybeErr TransactionErrResult, ctx *Context) bool
}
// TransactionScopeFunc the transaction's scope signature
type TransactionScopeFunc func(maybeErr TransactionErrResult, ctx *Context) bool
// EndTransaction ends the transaction with a callback to itself, implements the TransactionScope interface
func (tsf TransactionScopeFunc) EndTransaction(maybeErr TransactionErrResult, ctx *Context) bool {
return tsf(maybeErr, ctx)
}
// +------------------------------------------------------------+
// | Transaction Implementation |
// +------------------------------------------------------------+
// Transaction gives the users the opportunity to code their route handlers cleaner and safier
// it receives a scope which is decided when to send an error to the user, recover from panics
// stop the execution of the next transactions and so on...
//
// it's default scope is the TransientTransactionScope which is silently
// skips the current transaction's response if transaction.Complete accepts a non-empty error.
//
// Create and set custom transactions scopes with transaction.SetScope.
//
// For more information please visit the tests.
type Transaction struct {
context *Context
parent *Context
hasError bool
scope TransactionScope
}
func newTransaction(from *Context) *Transaction {
tempCtx := *from
writer := tempCtx.ResponseWriter().Clone()
tempCtx.ResetResponseWriter(writer)
t := &Transaction{
parent: from,
context: &tempCtx,
scope: TransientTransactionScope,
}
return t
}
// Context returns the current context of the transaction.
func (t *Transaction) Context() *Context {
return t.context
}
// SetScope sets the current transaction's scope
// iris.RequestTransactionScope || iris.TransientTransactionScope (default).
func (t *Transaction) SetScope(scope TransactionScope) {
t.scope = scope
}
// Complete completes the transaction
// rollback and send an error when the error is not empty.
// The next steps depends on its Scope.
//
// The error can be a type of context.NewTransactionErrResult().
func (t *Transaction) Complete(err error) {
maybeErr := TransactionErrResult{}
if err != nil {
t.hasError = true
statusCode := 400 // bad request
reason := err.Error()
cType := "text/plain; charset=" + t.context.Application().ConfigurationReadOnly().GetCharset()
if errWstatus, ok := err.(TransactionErrResult); ok {
if errWstatus.StatusCode > 0 {
statusCode = errWstatus.StatusCode
}
if errWstatus.Reason != "" {
reason = errWstatus.Reason
}
// get the content type used on this transaction
if cTypeH := t.context.GetContentType(); cTypeH != "" {
cType = cTypeH
}
}
maybeErr.StatusCode = statusCode
maybeErr.Reason = reason
maybeErr.ContentType = cType
}
// the transaction ends with error or not error, it decides what to do next with its Response
// the Response is appended to the parent context an all cases but it checks for empty body,headers and all that,
// if they are empty (silent error or not error at all)
// then all transaction's actions are skipped as expected
canContinue := t.scope.EndTransaction(maybeErr, t.context)
if !canContinue {
t.parent.SkipTransactions()
}
}
// TransientTransactionScope explanation:
//
// independent 'silent' scope, if transaction fails (if transaction.IsFailure() == true)
// then its response is not written to the real context no error is provided to the user.
// useful for the most cases.
var TransientTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
if maybeErr.IsFailure() {
ctx.Recorder().Reset() // this response is skipped because it's empty.
}
return true
})
// RequestTransactionScope explanation:
//
// if scope fails (if transaction.IsFailure() == true)
// then the rest of the context's response (transaction or normal flow)
// is not written to the client, and an error status code is written instead.
var RequestTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
if maybeErr.IsFailure() {
// we need to register a beforeResponseFlush event here in order
// to execute last the FireStatusCode
// (which will reset the whole response's body, status code and headers set from normal flow or other transactions too)
ctx.ResponseWriter().SetBeforeFlush(func() {
// we need to re-take the context's response writer
// because inside here the response writer is changed to the original's
// look ~context:1306
w := ctx.ResponseWriter().(*ResponseRecorder)
if maybeErr.Reason != "" {
// send the error with the info user provided
w.SetBodyString(maybeErr.Reason)
w.WriteHeader(maybeErr.StatusCode)
ctx.ContentType(maybeErr.ContentType)
} else {
// else execute the registered user error and skip the next transactions and all normal flow,
ctx.StopWithStatus(maybeErr.StatusCode)
}
})
return false
}
return true
})

View File

@ -7,15 +7,30 @@ import (
"syscall"
)
// RegisterOnInterrupt registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received.
// RegisterOnInterrupt registers a global function to call when CTRL+C pressed or a unix kill command received.
func RegisterOnInterrupt(cb func()) {
// var cb func()
// switch v := callbackFuncOrFuncReturnsError.(type) {
// case func():
// cb = v
// case func() error:
// cb = func() { v() }
// default:
// panic(fmt.Errorf("unknown type of RegisterOnInterrupt callback: expected func() or func() error but got: %T", v))
// }
Interrupt.Register(cb)
}
// Interrupt watches the os.Signals for interruption signals
// and fires the callbacks when those happens.
// A call of its `FireNow` manually will fire and reset the registered interrupt handlers.
var Interrupt = new(interruptListener)
var Interrupt TnterruptListener = new(interruptListener)
type TnterruptListener interface {
Register(cb func())
FireNow()
}
type interruptListener struct {
mu sync.Mutex

View File

@ -588,7 +588,7 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor {
if !app.config.DisableInterruptHandler {
// when CTRL/CMD+C pressed.
shutdownTimeout := 10 * time.Second
host.RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout))
RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout))
// app.logger.Debugf("Host: register server shutdown on interrupt(CTRL+C/CMD+C)")
}