This program will make panic not panicking but silently blocking forever.

func panicfunc() {
    panic("oops")
}

func run() {
    defer func() {
       select {}
    }
    panicfunc()
}

func main() {
        // not trigger runtime deadlock.
        go func() {
         ticker := time.NewTicker(10*time.Second) 
         select {
         case <-ticker.C:
         }
    }()
    run()
}

The common understanding is that the only way to prevent panic from 
propagating is to write "recover" function in the defer call chain, but due 
to that panic allows defers to be executed normally, it seems that 
accidentally blocking panicking propagation could become fairly trivial and 
subtle.

The above program is too obvious as an illustration, so it's likely not 
happening very often or sneaking out of some careful eyes, but in real 
situation, this might become much less obvious.  e.g. (stripped from our 
production code)

func (r *Resumer) FromCheckpoint(id string) {
     r.mu.Lock()
     defer r.mu.Unlock()
     ... mutate other fields of r...
     r.wg.Add(1)
    * r.resumeStarts[id] = time.Now()  *  
}

func (r *Resumer) WaitLoaded() {
      r.wg.Wait()
}

func (r *Resumer) RecreateFromCheckpoint() {
      for _, cp := r.checkpoint.Entries() {
            id := cp.GetId()
            r.FromCheckpoint(id)
            task := NewTask(id, cp)
            go task.Run()
      }
}

func (r *Runner) Start() {
       defer resumer.WaitLoaded()

       ... other logic ...
       if err := resumer.RecreateFromCheckpoint(); err != nil {
              os.Exit()
       }
    
       ... create RPC server...
       ... register HTTP handler...
       go server.Serve()
}

We added a new field in Resumer "resumeStarts" but forgot to put code to 
correctly initiate it, so when the code runs, it should panic, but 
unfortunately, it doesn't because we have a r.wg.Add(1) statement* *before** 
the 
map assignment and the wg.Wait is under a defer path, which will block 
panic propagation.

The most easiest fix is to just write

func (r *Resumer) FromCheckpoint(id string) {
     r.mu.Lock()
     defer r.mu.Unlock()
     ... mutate other fields of r...
    * r.resumeStarts[id] = time.Now()*
     r.wg.Add(1)
}

Completely arbitrary order without altering the program semantics and 
correctness but yet, write this way would not block the panic.

Because the symptom is a completely block and there is absolutely no 
recover call through out the stack, it was *SO NOT OBVIOUS* before we found 
the issue.

I am wonder if there is anything we can do to improve runtime support on 
panic propagation to make sure that other than the builtin "recover", 
nothing could prevent a panic unwinding the stack.


-- 
You received this message because you are subscribed to the Google Groups 
"golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/d/optout.

Reply via email to