Fix a warning about a bool > 0.
[qgit4/redivivus.git] / exception_manager.txt
blob83d070796c3977dded6f70af2d5f939aa5da8237
1                                       THE PROBLEM
4 Qt3 uses the call qApp->processEvents() (from now on PE) to let a program do not block while  
5 a long operation is occurring. This function passes the control to the application event loop
6 and any new pending event like GUI event, socket notifier(QProcess) and others are processed.
8 The problem with PE is that when the calling function returns a changed context could 
9 be found. Some objects could be born, other disappeared, some action incompatible
10 with the previous context could have been done, as example closing a window while 
11 processing or clearing a data container that the calling function is using.
13 How to deal with this? A way is to check some 'context flags' on return from
14 PE to search for a 'wrong context' and take appropriate actions.
15 But this is not a good general solution because implies that the function that calls, and
16 returns, from PE knows about the whole context.
18 As example, A() is a function that access some data (kept in any kind of container a list, a 
19 file, etc), B() is a function that makes a lengthy operation and uses PE.
20 If we have a code flow like the following
22     A()
23     |--->   B()
24     |        |-----> qApp->processEvents()
25     |        |-----> do something else
26     |                   return
27     |--->   ....
28     |--->   data.access()
30 We should check inside of B() if the data is no more valid and eventually return an error code.
31 But B() knows nothing about the our data. Of course we should check in A(), just after the B() call,
32 but this is sub-optimal because it implies that we know for sure that B() does not accesses the data
33 _nor_ any other function called by B() before to return to A().
35 In real software the call chain between the function that uses a resource and the function that
36 pass the control to the application event loop and return from it can be very long and complex.
40                                       INTRODUCING EXCEPTIONS
42 The standard way that C++ has to deal with this kind of problems is called exceptions handling.
44 If B() throws an exception, returning from PE, on a corrupted database, and the first catch clause
45 is in A(), no matter how long and complex the chain is, A() will catch the exception before anything
46 else is done.
48 This seems interesting, but has two drawbacks, one general, one depending on Qt.
51 -Exception resuming/termination models
53 What happens if B() is called on start up, on data refreshing, or, in general, in a context where database
54 consistency is not important or, worst, should not be checked? The exception will be thrown, no catch 
55 clause will take the exception, the default handler will be invoked and this means, at least in C++,
56 program termination [in C++ you can provide a custom set_terminate() function to handle uncaught 
57 exceptions but you cannot continue from here, just clean-up and exit].
58 The general problem has a name and is called 'C++ supports termination-model exceptions, not resumption'.
60 So we need a way to let B() throw an exception _only_ if the exception will be caught, IE
61 only if the abnormal condition is 'interesting' in the actual context.
64 -Exceptions trough signals/slots
66 Standard C++ try-throw-catch exception system is not compatible with Qt signals/slots. If a function B() is
67 called by A() by means of a signal the catch clause will not work, also if signals/slots in Qt3 are wrappers
68 to direct calls.
69          
70     A()
71     |---> try
72     |---> emit mySignal  --> slotMySignal() 
73     |                           |-----> throw X
74     |---> catch(x)
75     |      |---> we will NOT catch X
78 It is possible to code to avoid signals/slots when exceptions are involved, but the _real_ problem is
79 that also PE is a barrier to exceptions propagation.
81 What happens is the following:
83   A()
84     |---> try
85     |       B()
86     |        |-----> qApp->processEvents()
87     |        |                           |----> C()
88     |        |                                   |----> database.clear()
89     |        |                                throw databaseIsEmpty
90     |        |<----- qApp->processEvents()
91     |        | 
92     | <----return
93     | 
94     |---> catch(databaseIsEmpty)
95     |      |
96     |      |---> we will NOT catch databaseIsEmpty
99 This is very unfortunate.
103                                       INTRODUCING EXCEPTION MANAGER
105 If we rewrite the above scheme as follows:
107   A()
108     |---> try
109     |       B()
110     |        |-----> qApp->processEvents()
111     |        |                           |----> C()
112     |        |                                   |----> database.clear()
113     |        |                                throw databaseIsEmpty
114     |        |                                   |
115     |        |<----- qApp->processEvents()<----return
116     |        | 
117     |        if (databaseIsEmpty is throwable)
118     |              throw databaseIsEmpty
119     | <----return
120     | 
121     |---> catch(databaseIsEmpty)
122     |      |
123     |      |---> NOW we will catch databaseIsEmpty
127 Two things have changed between the schemes.
129       - The potential exception is checked to verify if it is among the throwables exceptions
131       - The exception is thrown in the same region* of the catch clause
134 *[A 'region' is the code that executes between two calls of PE]
137 Class ExceptionManager does exactly this, checks the exception against a throwable set, wait until
138 the correct region is reached and finally throws the exception.
140 If we rewrite the above code to use ExceptionManager helper macros we have:
142  A() {
143       .....
144       try {
145           EM_REGISTER(databaseIsEmpty); // adds databaseIsEmpty to the throwable set
147           .....
148           B();
149           .....
151           EM_REMOVE(databaseIsEmpty); // removes databaseIsEmpty from the throwable set
153       } catch (int i) {
155             EM_REMOVE(databaseIsEmpty);
157             if (i == databaseIsEmpty) {
159                    .....handle the exception....
161                    EM_CHECK_PENDING; // re-check any other pending exception
162             }   
163       }
164       .....
167 B() {
168       .....
169       EM_BEFORE_PROCESS_EVENTS; // some magic occurs ;-)
171       while(something_happens)
172           qApp->processEvents();
174       EM_AFTER_PROCESS_EVENTS; // throws the pending exceptions belonging to the current region
175       .....
178 C() {
179       .....
180       database.clear();
181       EM_RAISE(databaseIsEmpty); // checks if databaseIsEmpty is throwable and, in case,
182       .....                      // flags it as 'raised'. In the latter case it will be 
183       .....                      // thrown, but only when returning in the correct region.                       
187 With this scheme everything works as expected. There are some things to note:
189 1)  In B() there is no knowledge of 'databaseIsEmpty'. B() does not have to know about the general
190     context at all.
192 2)  At the end of the catch clause any other pending exception will be thrown, so to allow for
193     multiple raised exceptions.
195 3)  The same exception will be thrown as many times as has been registered. What it means is
196     ExceptionManager supports nested try-catch blocks, also when looking for the same exception,
197      in this case each catch clause will be called with the same exception and in the correct order.
199 4)  ExceptionManager has an internal multi-region stack. What it means is that try-catch blocks
200     can be nested _across_ many PE calls: each catch clause will be called with the correct raised
201     exception and in the correct time, when returning in the corresponding region from a PE call.
202     No matter when the exceptions have been raised.
206                                       TECHNICAL DETAILS
208 A 'region' is the code that executes between two calls of qApp->processEvents()
209 the code in a region has the stack property. Ie if fb() is called inside fa() then
210 fb() will return before fa().
212 A 'catch set' is a set of exceptions that are added at the beginning of the
213 same try block. Given a group of exceptions of the same catch set the following can occur:
215 1- No exception is raised -> the catch clause is not called.
217 2- Only one exception Ex of the set is raised -> the catch clause is called
218    with Ex parameter.
220 3- More then one exception Ex1, Ex2,..Exn are raised -> the catch clause is
221    called with Ex1 parameter, ie the first priority exception. The exception
222    priority is given when adding the exceptions at the beginning of try block.
223    The last added is the first priority.
226 The totalThrowableSet is a list of exceptions that can be raised with a call to
227 raise(excp).
229 The regionThrowableSet is a subset of totalThrowableSet and lists the exceptions
230 that can be thrown in the corresponding region.
232 The regionThrowableSet is saved before to call qApp->processEvents() and restored
233 on return.
235 A call to qApp->processEvents() trigger a region boundary. So a new and empty
236 regionThrowableSet must be used. To let ExceptionManager to know the region crossing time,
237 ie qApp->processEvents() call, we use the convention that the call to saveThrowableSet()
238 is done 'just before' the qApp->processEvents() call. Where 'just before' it means
239 that no others ExceptionManager method must be called between saveThrowableSet() and
240 processEvents().
242         int currentRegionId = saveThrowableSet();
244         .....(no more ExceptionManager calls).....
246         qApp->processEvents();
248         .....(no ExceptionManager calls).....
250         restoreThrowableSet(currentRegionId);
252 The region throwable sets are saved in a list: throwableSetList
254 When a call to raise(excp) occurs totalThrowableSet is walked to find excp.
255 If the exception is found then the exception is tagged to be thrown.
257 In this case the flag  isRaised is set in _all_ the occurrences of excp in the
258 regionThrowableSet and in _all_ the occurrences of excp in throwableSetList.
259 This is because the exception will be thrown in each region upon re-entering,
260 not only in the current region. And in the same region will be thrown as many
261 times as are the occurrences of excp in the corresponding throwable set
263 Upon restoring the throwable set with restoreThrowableSet() it is safe to
264 throw any pending exception with:
266         throwPending();
269 Method throwPending() walks _in order_ the regionThrowableSet ONLY to find all the
270 exceptions with the flag isRaised set.
272 This is because C++ throw-catch does not seem to be able to walk-back across
273 qApp->processEvents() boundaries, ie across regions. So _only_ the pending exceptions
274 of the current region will be thrown. The others will be eventually thrown later.
276 ExceptionManager throws ONLY ONE exception among the matching exceptions set. Then, in the
277 catch clause throwPending() is called again. This is to guarantee that exceptions are
278 thrown in the correct order.
279 Note that the catch clause is always in the same region of the throw command.
282                 The exception thrown is the last that has been added
285 -Removing exceptions from throwables list
287 Normally an exception is removed from throwables list by code when leaving try block. 
288 But thrown exceptions will by-pass try block and will go directly in the catch clause.
289 So all the exceptions added in try block must be removed in the catch clause, also if
290 the thrown exception is not handled there.
292 Note that in the catch clause all the exceptions of the catch set will be removed.
293 If there are two exceptions raised, the first will throw and the catch clause will
294 remove both. So the second exception will never be thrown. This is to take in account 
295 when adding exceptions in the try clause:
297                         NO!!!                                           YES
298         try {                                           try {
299                 EM_REGISTER(very_bad_one);                      EM_REGISTER(small_one);
300                 EM_REGISTER(small_one);                         EM_REGISTER(very_bad_one);
303 When a remove(excp) occurs regionThrowableSet and  totalThrowableSet are walked
304 in order from newest entry to oldest and the first occurrence of excp is removed.
305 So to take in account the case where the same exception is added twice:
307         fa() {
308                 ......
309                 try {
310                         EM_REGISTER(myExcp);
311                         .......(no processEvents() here, same region)
312                         fb();
313                         .......(no processEvents() here, same region)
314                         EM_REMOVE(myExcp);
315                 } catch ()
317 Where
318         fb() {
319                 ......
320                 try {
321                         EM_REGISTER(myExcp);
322                         .......
323                         EM_REMOVE(myExcp);
324                 } catch ()
327 In this case myExcp will be added twice and must be removed twice. And after the
328 first remove in fb() catch clause will be thrown again and caught in the fa() catch clause.
330 So at the end of the catch clause there must always be the throwPending() call.