The following code fails with an invalid memory address or nil pointer dereference error
. The use case is I need to send all requests through a queue, to ensure file operations occur in the right order. However, I cannot send the copy of the gin.Context
to the channel. I have also tried sending a callback function (that invokes the gin.Context
) with the same results.
package main
import "net/http"
import "github.com/gin-gonic/gin"
func main() {
router := gin.Default()
queue := make(chan *gin.Context)
go func() {
for {
c := <-queue
c.String(http.StatusOK, "")
}
}()
router.GET("/", func(c *gin.Context) {
queue <- c.Copy()
})
router.Run(":8080")
}
Comment From: nazwa
Copied context doesn't have access to output writer. Think of it as a read-only copy.
You would have to pass the original context there, but there might be some problems with this, like the context itself being released/reused and connection being closed the moment handler function ends.
@manucorporat should be able to give you more detail on this.
Comment From: frankandrobot
Yea when I pass the original context I get a "headers are already closed" error (I'm paraphrasing here). I'm quite open to the possibility that there is another way of doing this.
So yea, I can use mutexes but as per the godocs, that's not the preferred way of doing this.
Comment From: javierprovecho
@frankandrobot, as @nazwa said, you can't use the copied context to write a response, because the writer has been set to nil
when copying. Take a look here to see what is being copied and what not, https://github.com/gin-gonic/gin/blob/master/context.go#L73
Also, in your 2nd message, I'm very certain that you are getting headers are already closed
because there can be a delay between sending the context through the channel and using it, and in that moment, the chain of handlers has already finished and gin has closed the http request by writing the headers and everything else.
What I recommend for your exposed case in the 1st message, is to use mutexes (and atomic counters in case you need them). Opening a mutex at the start of your gin handler and closing it (or deferring) at the end, guarantees that requests won't be processed concurrently but at the same time you will not block the listener port for accepting new conns.
Hope that explanation helps you.
Comment From: frankandrobot
Yea see my previous answer
So yea, I can use mutexes but as per the godocs, that's not the preferred way of doing this.
So I guess I'll be looking for another framework....
Comment From: javierprovecho
@frankandrobot is not about the framework, but about how concurrency on golang works. You need to understand that in your example after passing the context through a channel, there is nothing left for gin to do rather than close the request. You need a way to hold the request at the handler, either by mutexes or with a callback channel signal.
closing as there is no issue with gin.
Comment From: frankandrobot
Yes, I understand how concurrency in golang works. My example code works just fine in Echo, which is a very similar framework.
Comment From: javierprovecho
@frankandrobot just tried it in case echo handle context differently and it does not. It fails almost the same, just that gin doesn't implement a recover()
when writing to an old context writer. How is your example code on echo? Mine is similar to what you posted above:
package main
import (
"net/http"
"github.com/labstack/echo"
)
func main() {
e := echo.New()
queue := make(chan echo.Context)
go func() {
for {
c := <-queue
c.String(http.StatusOK, "Hello, World!")
}
}()
e.GET("/", func(c echo.Context) error {
queue <- c
return nil
})
e.Logger.Fatal(e.Start(":1323"))
}
Comment From: YanRyan
I have the same problem with you. Do you find a solution?
Comment From: frenchtoasters
I found myself in a similar situation and was about to do something like the following:
var toolQueue = make(chan toolManager, 10)
func (t *toolManager) Action() (*toolManager, error) {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()
// do the action
}
func SendMessage(queue <-chan toolManager) {
for {
select {
case tool := <-queue:
tool.Action()
}
}
}
func router() http.Handler {
r := gin.New()
r.Use(gin.Recovery())
r.GET("/ping", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.POST("/tool/:name", toolPOST)
return r
}
func toolPOST(ctx *gin.Context) {
name := ctx.Param("name")
toolManager := &toolManager{
Name: name,
}
toolQueue <- *toolManager
ctx.JSON(http.StatusOK, gin.H{
"success": toolManager.Name,
})
}
func main() {
server := &http.Server{
Addr: ":8080",
Handler: router(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
g.Go(func() error {
return server.ListenAndServe()
})
go SendMessage(toolQueue)
if err := g.Wait(); err != nil {
log.Fatal(err)
}
}
This returns the api call almost instantly, however it queues up the action to be done in another thread, and you use the toolPost
function to parse the information from the request and build your object for the channel.