Ricorrente caso di Stack Overflow
L’applicazione ASP.NET in questione andava in crash di sovente con conseguente perdita di login e di sessione degli utenti collegati. Nell’EventLog veniva registrato il tipico messaggio di errore:
Error ID: 1009
A process serving application pool %1 was terminated unexpectedly. The process ID was %2. The process exit code was '0x%3.
La causa di tale problema sarebbe stata molto difficile, se non impossibile, da indagare se non tramite l’uso di un debugger.
Usando i Debugging Tools for Windows si può attaccare il debugger in crash mode al workerprocess usando adplus con i seguenti switch:
adplus -crash -pn w3wp.exe
Il debugger si metterà in ascolto sul processo intercettando l’eccezione non gestita e dumpando la memoria su disco.
Per questo post ho creato un esempietto di codice ad hoc per simulare lo scenario.
Aprendo il dump con WinDbg e caricando la SOS come visto nei post precedenti possiamo cominciare a guardare l’elenco dei trhead presenti al momento del crash:
0:028> !threads
ThreadCount: 9
UnstartedThread: 0
BackgroundThread: 9
PendingThread: 0
DeadThread: 0
Hosted Runtime: no
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
7 1 1290 0207d040 8220 Enabled 07378028:07379020 0207aa38 0 Ukn
22 2 2288 02084b00 b220 Enabled 00000000:00000000 0207aa38 0 MTA (Finalizer)
25 3 1f50 020ae080 80a220 Enabled 00000000:00000000 0207aa38 0 MTA (Threadpool Completion Port)
26 4 11cc 020aec50 1220 Enabled 00000000:00000000 0207aa38 0 Ukn
27 5 1598 020e3098 180b220 Enabled 0b29304c:0b294fe8 0207aa38 0 MTA (Threadpool Worker)
28 6 22a8 020ec5c0 180b220 Disabled 0f25afd4:0f25afe8 020af2f8 1 MTA (Threadpool Worker) System.StackOverflowException (072400a4) (nested exceptions)
29 7 f80 020f1390 180b220 Enabled 13240120:13240fe8 0207aa38 0 MTA (Threadpool Worker)
30 8 20bc 020f1760 180b220 Enabled 00000000:00000000 0207aa38 0 MTA (Threadpool Worker)
15 9 f20 02105918 880a220 Enabled 00000000:00000000 0207aa38 0 MTA (Threadpool Completion Port)
rker) System.StackOverflowException (072400a4) (nested exceptions)
29 7 f80 020f1390 180b220 Enabled 13240120:13240fe8 0207aa38 0 MTA (Threadpool Worker)
30 8 20bc 020f1760 180b220 Enabled 00000000:00000000 0207aa38 0 MTA (Threadpool Worker)
15 9 f20 02105918 880a220 Enabled 00000000:00000000 0207aa38 0 MTA (Threadpool Completion Port)
Si vede subito che il trhead 28 ha generato una System.StackOverflowException.
Possiamo dunque andarne ad analizzare lo stack. Prima ci posizioniamo sul thread 28 col comando ~28s e poi dumpiamo il managed stack col comando !clrstack:
0:028> ~28s
eax=ffffffff ebx=00000000 ecx=60865860 edx=0a53ff09 esi=00000001 edi=020ec974
eip=76c008ff esp=02d23448 ebp=02d234a8 iopl=0 nv up ei ng nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000284
kernel32!TerminateProcess:
76c008ff 8bff mov edi,edi0:028> !clrstack
*** ERROR: Symbol file could not be found. Defaulted to export symbols for mscorwks.dll -
PDB symbol for mscorwks.dll not loaded
OS Thread Id: 0x22a8 (28)
ESP EIP
02d23468 76c008ff [FaultingExceptionFrame: 02d23468]
02d23a1c 76c008ff [HelperMethodFrame: 02d23a1c]
02d23a74 01940976 _Default.LogException()
02d23aac 019408ef _Default.GlobalExceptionHandler()
02d23ae0 019409a5 _Default.LogException()
02d23b90 019408ef _Default.GlobalExceptionHandler()
[...cutting... ]
02d4e8a8 019409a5 _Default.LogException()
02d4e958 019408ef _Default.GlobalExceptionHandler()
02d4e98c 019409a5 _Default.LogException()
02d4ea3c 019408ef _Default.GlobalExceptionHandler()
02d4ea70 01940881 _Default.Button1_Click(System.Object, System.EventArgs)
02d4eb28 54b49ec8 System.Web.UI.WebControls.Button.OnClick(System.EventArgs)
02d4eb40 54b49d2f System.Web.UI.WebControls.Button.RaisePostBackEvent(System.String)
02d4eb58 54b49f6b System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(System.String)
02d4eb60 54445d9e System.Web.UI.Page.RaisePostBackEvent(System.Web.UI.IPostBackEventHandler, System.String)
02d4eb6c 54445cf5 System.Web.UI.Page.RaisePostBackEvent(System.Collections.Specialized.NameValueCollection)
02d4eb80 54457f1e System.Web.UI.Page.ProcessRequestMain(Boolean, Boolean)
02d4ecd8 544577a4 System.Web.UI.Page.ProcessRequest(Boolean, Boolean)
02d4ed10 544576d1 System.Web.UI.Page.ProcessRequest()
02d4ed48 54457666 System.Web.UI.Page.ProcessRequestWithNoAssert(System.Web.HttpContext)
02d4ed54 54457642 System.Web.UI.Page.ProcessRequest(System.Web.HttpContext)
02d4ed68 01940206 ASP.default_aspx.ProcessRequest(System.Web.HttpContext)
02d4ed78 5445db16 System.Web.HttpApplication+CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
02d4edac 5443132c System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
02d4edec 54a2531f System.Web.HttpApplication+PipelineStepManager.ResumeSteps(System.Exception)
02d4edf0 54a1b704 [InlinedCallFrame: 02d4edf0]
02d4ee90 54a0613d System.Web.HttpRuntime.ProcessRequestNotificationPrivate(System.Web.Hosting.IIS7WorkerRequest, System.Web.HttpContext)
02d4ef00 54ada7a2 System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)
02d4ef04 54ada58f [InlinedCallFrame: 02d4ef04]
02d4f458 013722e4 [NDirectMethodFrameStandalone: 02d4f458] System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr, System.Web.RequestNotificationStatus ByRef)
02d4f468 54ada839 System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr, IntPtr, IntPtr, Int32)
02d4f4ec 54ada58f System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr, IntPtr, IntPtr, Int32)
02d4f5ec 013722e4 [ContextTransitionFrame: 02d4f5ec]
A dire il vero sopra riporto solo parte dello stack, tagliando la parte che si ripete uguale a se stessa per decine di volte [..cutting..].
Si nota subito la ricorrenza consistente nelle chiamate alle funzioni:
_Default.GlobalExceptionHandler()
e
_Default.LogException()
In pratica succede quanto segue:
- l’applicazione genera un’eccezione che viene intercettata ed inviata alla routine di gestione delle eccezioni.
- tale routine elabora l’eccezione e la invia poi ad un’altra routine che la logga.
- in quest’ultima routine si genera un seconda eccezione (la scrittura del log non va a buon fine) e tale eccezione finisce nuovamente nel global exception handler.
- quest’ultimo elabora ancora l’eccezione inviandola nuovamente al logger che, loggandola, incappa nuovamente nell’errore
- il ciclo si ripete all’infinito e lo stack si riempie.
Lo stack da me mostrato, generato con il mio codice di esempio qui sotto e contenente solo due funzioni ricorrenti, è in verità molto più semplice di quello che avveniva nell’applicazione reale, ma il concetto è il medesimo.
Partial Class _Default
Inherits System.Web.UI.Page
Protected Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
Try
Throw New System.Exception
Catch ex As Exception
Call GlobalExceptionHandler()
End Try
End Sub
Private Sub GlobalExceptionHandler()
Try
'do work
Call LogException()
Catch ex As Exception
GlobalExceptionHandler()
End Try
End Sub
Private Sub LogException()
Try
'do work
Throw New System.Exception
Catch ex As Exception
GlobalExceptionHandler()
End Try
End Sub
End Class
Occorre dunque evitare che, all’interno del gestore di eccezioni, vengano generate eccezioni gestite con la medesima routine in modo da spezzare la ricorrenza infinita.
Seppure il concetto sembri banale, devo constatare che problemi analoghi accadono abbastanza di frequente. E’ segno dunque di quanto sia facile commettere sviste di questo tipo.
Il problema che ne consegue è poi piuttosto subdolo: ovviamente non si riproduce mai in fase di developing e testing, e tende poi a presentarsi randomicamente in produzione quando per puro caso le due eccezioni si verificano contemporaneamente.
Alla prossima
Stefano