More on Transactions: Fallback on, unexpected, panics and able to send 'silent' error which stills reverts the changes but no output

This commit is contained in:
Gerasimos (Makis) Maropoulos 2016-12-18 14:08:28 +02:00
parent 1da8231abd
commit 88c98bb1e1
5 changed files with 129 additions and 19 deletions

View File

@ -2,6 +2,9 @@
**How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`.
## 5.1.1 -> 5.1.3
- **More on Transactions vol 3**: Recovery from any (unexpected error) panics inside `context.BeginTransaction` without loud, continue the execution as expected. Next version will have a little cleanup if I see that the transactions code is going very large or hard to understand the flow*
## 5.1.1 -> 5.1.2
- **More on Transactions vol 2**: Added **iris.UseTransaction** and **iris.DoneTransaction** to register transactions as you register middleware(handlers). new named type **iris.TransactionFunc**, shortcut of `func(scope *iris.TransactionScope)`, that gives you a function which you can convert a transaction to a normal handler/middleware using its `.ToMiddleware()`, for more see the `test code inside context_test.go:TestTransactionsMiddleware`.

View File

@ -20,7 +20,7 @@
<br/>
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.1.2%20-blue.svg?style=flat-square" alt="Releases"></a>
<a href="https://github.com/kataras/iris/releases"><img src="https://img.shields.io/badge/%20version%20-%205.1.3%20-blue.svg?style=flat-square" alt="Releases"></a>
<a href="https://github.com/iris-contrib/examples"><img src="https://img.shields.io/badge/%20examples-repository-3362c2.svg?style=flat-square" alt="Examples"></a>
@ -920,7 +920,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning
------------
Current: **v5.1.2**
Current: **v5.1.3**
Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)**

View File

@ -1122,6 +1122,23 @@ func (ctx *Context) MaxAge() int64 {
return -1
}
// ErrFallback is just an empty error but it is recognised from the TransactionScope.Complete,
// it reverts its changes and continue as normal, no error will be shown to the user.
//
// Usually it is used on recovery from panics (inside .BeginTransaction)
// but users can use that also to by-pass the error's response of your custom transaction pipe.
type ErrFallback struct{}
func (ne *ErrFallback) Error() string {
return ""
}
// NewErrFallback returns a new error wihch contains an empty error,
// look .BeginTransaction and context_test.go:TestTransactionRecoveryFromPanic
func NewErrFallback() *ErrFallback {
return &ErrFallback{}
}
// ErrWithStatus custom error type which is useful
// to send an error containing the http status code and a reason
type ErrWithStatus struct {
@ -1131,6 +1148,11 @@ type ErrWithStatus struct {
message string // if it's empty then the already registered custom(or default) http error will be fired.
}
// Silent in case the user changed his/her mind and wants to silence this error
func (err *ErrWithStatus) Silent() error {
return NewErrFallback()
}
// Status sets the http status code of this error
// if only status exists but no reason then
// custom http error of this staus (if any) will be fired (context.EmitError)
@ -1215,7 +1237,12 @@ func (r *TransactionScope) Complete(err error) {
ctx := r.Context
statusCode := StatusInternalServerError // default http status code if not provided
reason := err.Error()
if _, ok := err.(*ErrFallback); ok {
// revert without any log or response.
r.isFailure = true
ctx.Response.Reset()
return
}
if errWstatus, ok := err.(*ErrWithStatus); ok {
if errWstatus.statusCode > 0 {
// get the status code from the custom error type
@ -1303,11 +1330,17 @@ func (pipe TransactionFunc) ToMiddleware() HandlerFunc {
}
}
// non-detailed error log for transacton unexpected panic
var errTransactionInterrupted = errors.New("Transaction Interrupted, recovery from panic:\n%s")
// BeginTransaction starts a request scoped transaction.
// Transactions have their own middleware ecosystem also, look iris.go:UseTransaction.
//
// See https://github.com/iris-contrib/examples/tree/master/transactions for more
func (ctx *Context) BeginTransaction(pipe TransactionFunc) {
func (ctx *Context) BeginTransaction(pipe func(scope *TransactionScope)) {
// SILLY NOTE: use of manual pipe type in order of TransactionFunc
// in order to help editors complete the sentence here...
// do NOT begin a transaction when the previous transaction has been failed
// and it was requested scoped or SkipTransactions called manually.
if ctx.TransactionsSkipped() {
@ -1318,24 +1351,34 @@ func (ctx *Context) BeginTransaction(pipe TransactionFunc) {
tempCtx := *ctx
// get a transaction scope from the pool by passing the temp context/
scope := acquireTransactionScope(&tempCtx)
defer func() {
if err := recover(); err != nil {
if ctx.framework.Config.IsDevelopment {
ctx.Log(errTransactionInterrupted.Format(err).Error())
}
// complete (again or not , doesn't matters) the scope without loud
scope.Complete(NewErrFallback())
// we continue as normal, no need to return here*
}
// if the transaction completed with an error then the transaction itself reverts the changes
// and replaces the context's response with an error.
// if the transaction completed successfully then we need to pass the temp's context's response to this context.
// so we must copy back its context at all cases, no matter the result of the transaction.
*ctx = *scope.Context
// if the scope had lifetime of the whole request and it completed with an error(failure)
// then we do not continue to the next transactions.
if scope.isRequestScoped && scope.isFailure {
ctx.SkipTransactions()
}
// finally, release and put the transaction scope back to the pool.
releaseTransactionScope(scope)
}()
// run the worker with its context inside this scope.
pipe(scope)
// if the transaction completed with an error then the transaction itself reverts the changes
// and replaces the context's response with an error.
// if the transaction completed successfully then we need to pass the temp's context's response to this context.
// so we must copy back its context at all cases, no matter the result of the transaction.
*ctx = *scope.Context
// if the scope had lifetime of the whole request and it completed with an error(failure)
// then we do not continue to the next transactions.
if scope.isRequestScoped && scope.isFailure {
ctx.SkipTransactions()
}
// finally, release and put the transaction scope back to the pool.
releaseTransactionScope(scope)
}
// Log logs to the iris defined logger

View File

@ -972,5 +972,69 @@ func TestTransactionsMiddleware(t *testing.T) {
ContentType("text/html", api.Config.Charset).
Body().
Equal(expectedResponse)
}
func TestTransactionFailureCompletionButSilently(t *testing.T) {
iris.ResetDefault()
expectedBody := "I don't care for any unexpected panics, this response should be sent."
iris.Get("/panic_silent", func(ctx *iris.Context) {
ctx.BeginTransaction(func(scope *iris.TransactionScope) {
scope.Context.Write("blablabla this should not be shown because of 'unexpected' panic.")
panic("OMG, UNEXPECTED ERROR BECAUSE YOU ARE NOT A DISCIPLINED PROGRAMMER, BUT IRIS HAS YOU COVERED!")
})
ctx.WriteString(expectedBody)
})
iris.Get("/expected_error_but_silent_instead_of_send_the_reason", func(ctx *iris.Context) {
ctx.BeginTransaction(func(scope *iris.TransactionScope) {
scope.Context.Write("this will not be sent.")
// complete with a failure ( so revert the changes) but do it silently.
scope.Complete(iris.NewErrFallback())
})
ctx.WriteString(expectedBody)
})
iris.Get("/silly_way_expected_error_but_silent_instead_of_send_the_reason", func(ctx *iris.Context) {
ctx.BeginTransaction(func(scope *iris.TransactionScope) {
scope.Context.Write("this will not be sent.")
// or if you know the error will be silent from the beggining: err := &iris.ErrFallback{}
err := iris.NewErrWithStatus()
fail := true
if fail {
err.Status(iris.StatusBadRequest).Reason("we dont know but it was expected error")
}
// we change our mind we don't want to send the error to the user, so err.Silent to the .Complete
// complete with a failure ( so revert the changes) but do it silently.
scope.Complete(err.Silent())
})
ctx.WriteString(expectedBody)
})
e := httptest.New(iris.Default, t)
e.GET("/panic_silent").Expect().
Status(iris.StatusOK).
Body().
Equal(expectedBody)
e.GET("/expected_error_but_silent_instead_of_send_the_reason").
Expect().
Status(iris.StatusOK).
Body().
Equal(expectedBody)
e.GET("/silly_way_expected_error_but_silent_instead_of_send_the_reason").
Expect().
Status(iris.StatusOK).
Body().
Equal(expectedBody)
}

View File

@ -80,7 +80,7 @@ const (
// IsLongTermSupport flag is true when the below version number is a long-term-support version
IsLongTermSupport = false
// Version is the current version number of the Iris web framework
Version = "5.1.2"
Version = "5.1.3"
banner = ` _____ _
|_ _| (_)