DIRECTORY AlpineEnvironment, AlpineInternal, BasicTime, List, Lock, LockControl, LockInternal, Process, Rope USING [Cat, Concat, ROPE], SafeStorage, SkiPatrolHooks USING [TransIDFromTransHandle, TransIDToRope], SkiPatrolLog USING [notice, TransactionAbortInfo], TransactionMap; LockWatchdogImpl: PROGRAM IMPORTS BasicTime, List, Lock, LockInternal, Process, Rope, SafeStorage, SkiPatrolHooks, SkiPatrolLog, TransactionMap EXPORTS AlpineInternal, LockControl = BEGIN LockID: TYPE = Lock.LockID; nullLockID: LockID = Lock.nullLockID; LockMode: TYPE = Lock.LockMode; ModeReleasableSet: TYPE = Lock.ModeReleasableSet; Handle: TYPE = LockInternal.Handle; Object: TYPE = LockInternal.Object; HeaderHandle: TYPE = LockInternal.HeaderHandle; RequestHandle: TYPE = LockInternal.RequestHandle; GrantedRequestHandle: TYPE = LockInternal.GrantedRequestHandle; WaitingRequestHandle: TYPE = LockInternal.WaitingRequestHandle; LockTransHeaderHandle: TYPE = LockInternal.LockTransHeaderHandle; ConflictingTranses: TYPE = RECORD[holder: TransactionMap.Handle, waiter: TransactionMap.Handle]; -- typical information about a lock conflict LockTransHeaderObject: PUBLIC TYPE = LockInternal.Object.request.transHeader; z: ZONE _ SafeStorage.GetSystemZone[]; ForkWatchdogProcess: PUBLIC PROC [ wakeupPeriod: Process.Milliseconds, abortWaitingRequestInterval: INT--seconds--, abortInactiveGrantedRequestInterval: INT--seconds--] = { Process.Detach[FORK LockWatchdogProcess[wakeupPeriod, abortWaitingRequestInterval, abortInactiveGrantedRequestInterval]]; }; LockWatchdogProcess: PROC [ wakeupPeriod: Process.Milliseconds, abortWaitingRequestInterval: INT--seconds--, abortInactiveGrantedRequestInterval: INT--seconds--] = { wakeupPeriodsUntilTimeoutCheck: INT _ 0; numWaits: INT _ 0; p: Path _ NIL; DO Process.Pause[Process.MsecToTicks[wakeupPeriod] ! ABORTED => GOTO aborted]; IF (wakeupPeriodsUntilTimeoutCheck _ wakeupPeriodsUntilTimeoutCheck-1) <= 0 THEN { l: LIST OF ConflictingTranses; logProc: PROC [SkiPatrolLog.TransactionAbortInfo]; -- (used to stop race condition) [wakeupPeriodsUntilTimeoutCheck, l] _ ChooseTimeoutVictims[wakeupPeriod, abortWaitingRequestInterval]; UNTIL l = NIL DO lNext: LIST OF ConflictingTranses _ l.rest; IF (logProc _ SkiPatrolLog.notice.abortTransaction) # NIL THEN { message: Rope.ROPE; message _ IF l.first.holder # NIL THEN Rope.Cat["transaction timed out on lock held by ", SkiPatrolHooks.TransIDToRope[SkiPatrolHooks.TransIDFromTransHandle[l.first.holder]]] ELSE "transaction timed out; livelock?"; logProc[[ transID: SkiPatrolHooks.TransIDFromTransHandle[l.first.waiter], where: "LockWatchdogImpl.LockWatchdogProcess", why: watchDog, message: ]]; }; TransactionMap.AbortUnilaterally[l.first.waiter, timeout]; Process.Yield[]; FREE[@l]; l _ lNext; ENDLOOP; }; Process.Yield[]; DO l: LIST OF ConflictingTranses _ GetBlockingTransactions[]; currentTime: BasicTime.GMT _ BasicTime.Now[]; noAbortDone: BOOL _ TRUE; UNTIL l = NIL DO lNext: LIST OF ConflictingTranses _ l.rest; elapsedTimeSinceLastStartWork: INT _ BasicTime.Period[from: TransactionMap.GetTimeOfLastStartWork[l.first.holder], to: currentTime]; IF elapsedTimeSinceLastStartWork > abortInactiveGrantedRequestInterval THEN { logProc: PROC [SkiPatrolLog.TransactionAbortInfo]; IF (logProc _ SkiPatrolLog.notice.abortTransaction) # NIL THEN logProc[[ transID: SkiPatrolHooks.TransIDFromTransHandle[l.first.holder], where: "LockWatchdogImpl.LockWatchdogProcess", why: watchDog, message: Rope.Cat[ "trans is holding up ", SkiPatrolHooks.TransIDToRope[SkiPatrolHooks.TransIDFromTransHandle[l.first.waiter]] ] ]]; TransactionMap.AbortUnilaterally[l.first.holder, blockingNewLockRequest]; Process.Yield[]; noAbortDone _ FALSE; }; FREE[@l]; l _ lNext; ENDLOOP; IF noAbortDone THEN EXIT; ENDLOOP; Process.Yield[]; { g: WaitingForGraph; [numWaits, g] _ ComputeWaitingForGraph[numWaits]; IF g # NIL THEN { logProc: PROC [SkiPatrolLog.TransactionAbortInfo]; -- (used to stop race condition) DO t: TransactionMap.Handle; p _ FindCycle[g, p]; IF (t _ ChooseVictim[p]) = NIL THEN GOTO freeGraph; IF (logProc _ SkiPatrolLog.notice.abortTransaction) # NIL THEN { deadlockList: Rope.ROPE _ NIL; FOR i: NAT IN [0 .. p.length) DO deadlockList _ deadlockList.Cat[ " ", SkiPatrolHooks.TransIDToRope[SkiPatrolHooks.TransIDFromTransHandle[p[i].waitingTrans.trans]] ]; ENDLOOP; logProc[[ transID: SkiPatrolHooks.TransIDFromTransHandle[t], where: "LockWatchdogImpl.LockWatchdogProcess", why: watchDog, message: Rope.Concat["deadlock of", deadlockList] ]]; }; TransactionMap.AbortUnilaterally[t, deadlock]; Process.Yield[]; g _ EliminateTrans[g, t]; REPEAT freeGraph => FreeGraph[g]; ENDLOOP; }; }; ENDLOOP; EXITS aborted => NULL }; WaitMemb: PROC [trans: TransactionMap.Handle, list: LIST OF ConflictingTranses] RETURNS [BOOLEAN] ~ { UNTIL list = NIL DO IF trans = list.first.waiter THEN RETURN [TRUE]; list _ list.rest ENDLOOP; RETURN [FALSE] }; BlockMemb: PROC [trans: TransactionMap.Handle, list: LIST OF ConflictingTranses] RETURNS [BOOLEAN] ~ { UNTIL list = NIL DO IF trans = list.first.holder THEN RETURN [TRUE]; list _ list.rest ENDLOOP; RETURN [FALSE] }; ChooseTimeoutVictims: PROC [wakeupPeriod: INT--msec--, abortWaitingRequestInterval: INT--seconds--] RETURNS [wakeupPeriodsUntilTimeoutCheck: INT, l: LIST OF ConflictingTranses] = { currentTime: BasicTime.GMT = BasicTime.Now[]; secondsToNextWakeup: INT _ abortWaitingRequestInterval; NoticeWaitingRequest: PROC [wr: WaitingRequestHandle] RETURNS [stop: BOOL] = { m: LockMode = ModeAfterGrantingRequest[wr]; elapsedTimeSinceWait: INT--seconds-- = BasicTime.Period[from: wr.startTime, to: currentTime]; IF elapsedTimeSinceWait >= abortWaitingRequestInterval THEN { IF NOT WaitMemb[wr.trans, l] THEN { FOR h: Handle _ wr.requestList, h.requestList UNTIL h=wr DO WITH h SELECT FROM grh: GrantedRequestHandle => IF grh.trans # wr.trans AND NOT Lock.Compat[m][grh.mode] THEN { l _ z.CONS[first: [holder: grh.trans, waiter: wr.trans], rest: l]; GOTO FoundABlocker } ENDCASE; REPEAT FoundABlocker => NULL; FINISHED => l _ z.CONS[first: [holder: NIL, waiter: wr.trans], rest: l]; ENDLOOP; } } ELSE { secondsToNextWakeup _ MIN[secondsToNextWakeup, abortWaitingRequestInterval-elapsedTimeSinceWait]; }; RETURN [stop: FALSE]; }; -- NoticeWaitingRequest l _ NIL; LockInternal.GetInfo[waitingRequestEnumProc: NoticeWaitingRequest]; RETURN [(secondsToNextWakeup*1000 + wakeupPeriod-1)/wakeupPeriod, l]; }; GetBlockingTransactions: PROC [] RETURNS [l: LIST OF ConflictingTranses] = { NoticeWaitingRequest: PROC [wr: WaitingRequestHandle] RETURNS [stop: BOOL] = { m: LockMode = ModeAfterGrantingRequest[wr]; FOR h: Handle _ wr.requestList, h.requestList UNTIL h=wr DO WITH h SELECT FROM grh: GrantedRequestHandle => IF grh.trans # wr.trans AND NOT Lock.Compat[m][grh.mode] AND NOT BlockMemb[grh.trans, LOOPHOLE[l]] THEN l _ z.CONS[first: [holder: grh.trans, waiter: wr.trans], rest: l]; ENDCASE; ENDLOOP; RETURN [stop: FALSE]; }; l _ NIL; LockInternal.GetInfo[waitingRequestEnumProc: NoticeWaitingRequest]; RETURN [l]; }; ModeAfterGrantingRequest: PROC [wr: WaitingRequestHandle] RETURNS [LockMode] = { FOR h: Handle _ wr.requestList, h.requestList UNTIL h=wr DO WITH h SELECT FROM grh: GrantedRequestHandle => IF grh.trans = wr.trans THEN RETURN[Lock.Sup[wr.mode][grh.mode]]; ENDCASE; ENDLOOP; RETURN [wr.mode]; }; WaitingTransHandle: TYPE = REF WaitingTransObject; WaitingTransObject: TYPE = RECORD [ trans: TransactionMap.Handle, edges: LIST OF--WaitingTransHandle--REF ANY, visited: BOOL _ FALSE, onPath: BOOL _ FALSE ]; WaitingForGraph: TYPE = LIST OF--WaitingTransHandle--REF ANY; ComputeWaitingForGraph: PROC [numWaits: INT] RETURNS [newNumWaits: INT, g: WaitingForGraph] = { NoticeGeneralInfo: LockInternal.GeneralInfoProc = { newNumWaits _ nSetCallsWaited; }; NoticeWaitingRequest: PROC [wr: WaitingRequestHandle] RETURNS [stop: BOOL] = { IF newNumWaits = numWaits THEN RETURN [stop: TRUE] ELSE { IF (LookupTrans[g, wr.trans] = NIL) THEN { wt: WaitingTransHandle _ z.NEW[WaitingTransObject _ [trans: wr.trans, edges: NIL]]; g _ z.CONS[first: wt, rest: g]; }; RETURN [stop: FALSE]; }; }; NoticeWaitingRequest2: PROC [wr: WaitingRequestHandle] RETURNS [stop: BOOL] = { IF newNumWaits = numWaits THEN RETURN [stop: TRUE] ELSE { waiting: WaitingTransHandle = LookupTrans[g, wr.trans]; m: LockMode = ModeAfterGrantingRequest[wr]; FOR h: Handle _ wr.requestList, h.requestList UNTIL h =wr DO WITH h SELECT FROM grh: GrantedRequestHandle => IF grh.trans # wr.trans AND NOT Lock.Compat[m][grh.mode] THEN { granted: WaitingTransHandle _ LookupTrans[g, grh.trans]; IF granted # NIL AND NOT List.Memb[granted, waiting.edges] THEN waiting.edges _ z.CONS[first: granted, rest: waiting.edges]; }; ENDCASE; ENDLOOP; RETURN [stop: FALSE]; }; }; g _ NIL; LockInternal.GetInfo[ generalInfoProc: NoticeGeneralInfo, waitingRequestEnumProc: NoticeWaitingRequest, waitingRequestEnumProc2: NoticeWaitingRequest2]; RETURN [newNumWaits, g]; }; PathRecord: TYPE = RECORD [ length: NAT _ 0, vertices: SEQUENCE maxLength: NAT OF RECORD [ waitingTrans: WaitingTransHandle, nextEdge: LIST OF--WaitingTransHandle--REF ANY ] ]; Path: TYPE = REF PathRecord; FindCycle: PROC [g: WaitingForGraph, oldPath: Path] RETURNS [path: Path] = { top: LIST OF REF ANY _ g; pathLast: NAT _ 0; v: WaitingTransHandle; pathMaxLength: NAT = List.Length[g]; path _ IF oldPath # NIL AND oldPath.length >= pathMaxLength THEN oldPath ELSE z.NEW[PathRecord[pathMaxLength]]; DO { -- EXITS NextTopLevelVertex v _ NARROW[top.first]; IF v.visited THEN GOTO NextTopLevelVertex; DO path[pathLast] _ [waitingTrans: v, nextEdge: v.edges]; v.visited _ v.onPath _ TRUE; DO e: LIST OF REF ANY _ path[pathLast].nextEdge; IF e = NIL THEN { path[pathLast].waitingTrans.onPath _ FALSE; IF pathLast = 0 THEN GOTO NextTopLevelVertex; pathLast _ pathLast - 1; } ELSE { path[pathLast].nextEdge _ e.rest; v _ NARROW[e.first]; IF v.onPath THEN GOTO FoundCycle; IF NOT v.visited THEN { pathLast _ pathLast + 1; EXIT; }; }; ENDLOOP; ENDLOOP; EXITS NextTopLevelVertex => IF (top _ top.rest) = NIL THEN { path.length _ 0; GOTO done; }; FoundCycle => { cycleStart: NAT; FOR cycleStart _ 0, cycleStart+1 UNTIL path[cycleStart].waitingTrans = v DO ENDLOOP; FOR j: NAT IN [cycleStart .. pathLast] DO path[j-cycleStart] _ [path[j].waitingTrans, NIL]; ENDLOOP; path.length _ pathLast-cycleStart+1; GOTO done; }; }; ENDLOOP; EXITS done => FOR j: NAT IN [path.length .. path.maxLength) DO path[j] _ [NIL, NIL]; ENDLOOP; }; ChooseVictim: PROC [p: Path] RETURNS [TransactionMap.Handle] = { minUpdateCost: INT _ LAST[INT]; minUpdateCostTrans: TransactionMap.Handle _ NIL; FOR i: NAT IN [0 .. p.length) DO updateCost: INT _ TransactionMap.GetEstimatedUpdateCost[p[i].waitingTrans.trans]; IF updateCost < minUpdateCost OR minUpdateCostTrans = NIL THEN { minUpdateCost _ updateCost; minUpdateCostTrans _ p[i].waitingTrans.trans; }; ENDLOOP; RETURN [minUpdateCostTrans]; }; LookupTrans: PROC [g: WaitingForGraph, t: TransactionMap.Handle] RETURNS [wt: WaitingTransHandle] = { FOR l: LIST OF REF ANY _ g, l.rest UNTIL l = NIL DO wt: WaitingTransHandle _ NARROW[l.first]; IF wt.trans = t THEN RETURN [wt]; ENDLOOP; RETURN [NIL]; }; EliminateTrans: PROC [g: WaitingForGraph, t: TransactionMap.Handle] RETURNS [newG: WaitingForGraph]= { wt: WaitingTransHandle _ LookupTrans[g, t]; FreeList[wt.edges]; wt.edges _ NIL; FOR l: LIST OF REF ANY _ g, l.rest UNTIL l = NIL DO w: WaitingTransHandle _ NARROW[l.first]; w.visited _ w.onPath _ FALSE; w.edges _ List.DRemove[wt, w.edges]; ENDLOOP; RETURN [List.DRemove[wt, g]]; }; FreeGraph: PROC [g: WaitingForGraph] = { FOR l: LIST OF REF ANY _ g, l.rest UNTIL l = NIL DO wt: WaitingTransHandle _ NARROW[l.first]; FreeList[wt.edges]; FREE[@wt]; ENDLOOP; FreeList[g]; }; FreeList: PROC [l: LIST OF REF ANY] = { UNTIL l = NIL DO lNext: LIST OF REF ANY _ l.rest; FREE[@l]; l _ lNext; ENDLOOP; }; END.--LockWatchdogImpl CHANGE LOG Changed by MBrown on February 6, 1983 10:03 pm BLockWatchdogImpl.mesa Copyright c 1984 by Xerox Corporation. All rights reserved. Last edited by MBrown on January 30, 1984 5:41:50 pm PST Last Edited by: Kupfer, August 6, 1984 3:31:59 pm PDT NOTES Some care has been taken to make these procedures efficient in the (normal) case that waiting happens rarely, the total number of waiting requests is small, and SkiPatrol logging is turned off for abortTransaction. This allows us to check more frequently, which improves response in cases when the wait can be resolved right away (deadlock and inactive blocking transaction). AlpineInternal.LockTransHeaderObject LockControl.ForkWatchdogProcess. For each waiting request, abort its transaction if it has waited for more than abortWaitingRequestInterval seconds. (Of course, don't bother to check for timed-out requests unless a request might be timed out, based on information obtained on the last check.) Collect a list of transactions that hold locks that other transactions need. For each blocking transaction: if it has done no work in the last abortInactiveGrantedRequestInterval seconds, abort it. Repeat until no transaction is aborted by this test. Compute a graph whose vertices are transactions with one or more waiting lock requests, containing a directed edge from t1 to t2 if t2 holds a lock that prevents a waiting lock request or t1 from being satisfied. (Don't bother to compute the graph if no lock requests have waited since the last time the graph was computed.) If the graph contains a cycle, find a cycle and abort the lowest-cost transaction in the cycle. Repeat until the graph contains no cycle. Log the transaction ID's of everyone involved in the deadlock. Support for timeout and blockingNewLockRequest detection. Tells whether "trans" appears as a "waiter" in "list". Tells whether "trans" appears as a "holder" in "list". INTERNAL to LockCoreImpl If "wr" is old and not already in our list of things to kill (l), then add it to the list. When we add it, we'd like to record a transaction ID for a transaction that is blocking it (there may be more than one). If we can't find a holder, record NIL as the holder (but kill the waiter anyway). wr is old and not already in the list, so find a holder wr isn't old; update time for next wakeup, if necessary INTERNAL to LockCoreImpl Look for transactions that are blocking wr. As one is found, add it to the list if it isn't already in it. INTERNAL to LockCoreImpl Support for deadlock detection. If numWaits equals the number of lock waits reported by the lock manager, return newNumWaits = numWaits, g = NIL. Otherwise compute the current waiting-for graph, and return it as g. If there are new waiters, add this transaction to the list of waiting transactions. If there are new waiters, create an edge for each transaction blocking this request. If g contains a cycle, returns a cycle as path; returns path.length = 0 if g contains no cycle. Reuses oldPath's storage if it can. Add the new vertex v to the end of the path. Explore the next edge out of the vertex at the end of the path. Remove a vertex from the end of the path. Add v to the end of the path. Move vertices of cycle down to start of sequence, clean up rest, and return. Transactions in p are deadlocked. Choose one to be aborted. Return NIL iff p is empty. Compute a new graph, eliminating t. Mark all vertices unvisited and not on path. NILs things out (superstition). Bug in GetBlockingTransactions: always returned the waiting transaction instead of the blocking transaction. This meant wait => abort nearly every time. Edited on July 24, 1984 11:01:18 am PDT, by Kupfer Add SkiPatrolLog probes. changes to: LockWatchdogImpl, LockWatchdogProcess Edited on July 29, 1984 7:08:42 pm PDT, by Kupfer Add code so that the lockConflict probes can report who the interested parties were. changes to: LockWatchdogImpl, ChooseTimeoutVictims, NoticeWaitingRequest (local of ChooseTimeoutVictims), DIRECTORY Edited on August 6, 1984 3:31:53 pm PDT, by Kupfer Remove the possible race condition in SkiPatrolLog probes by assigning the PROC to a temporary variable. changes to: DIRECTORY, LockWatchdogImpl Κ·˜Icodešœ™Kšœ Οmœ1™šœ ˜ K˜?Kšœ.˜.Kšœ˜˜K˜KšœS˜SK˜—K˜——K˜IK˜Kšœžœ˜K˜—Kšžœ˜ K˜ Kšžœ˜—Kšžœ žœžœ˜Kšžœ˜K˜—KšœΡ™ΡK˜˜K˜K˜1šžœžœžœ˜Kšœ žœ'Ÿ ˜Tšž˜K˜K˜Kšžœžœžœžœ ˜3šžœ4žœž˜@Kšœ>™>Kšœžœžœ˜šžœžœžœž˜ ˜ K˜Kšœ\˜\K˜—Kšžœ˜—šœ ˜ K˜2Kšœ.˜.Kšœ˜K˜1K˜—K˜—K˜.K˜K˜šž˜K˜—Kšžœ˜—K˜—K˜K˜—Kšžœ˜—šž˜Kšœ ž˜—K˜—K˜—˜Iheadšœ9™9K˜š  œžœ&žœžœžœžœ˜eJšœ6™6šžœžœž˜Jšžœžœžœžœ˜0J˜Jšžœ˜—Jšžœžœ˜J˜J˜—š   œžœ&žœžœžœžœ˜fJšœ6™6šžœžœž˜Jšžœžœžœžœ˜0J˜Jšžœ˜—Jšžœžœ˜J˜J˜—K˜š œžœžŸœžŸ œžœ"žœžœžœ˜΄Kšœžœ˜-Kšœžœ˜7unitš œžœžœžœ˜NKšœ™K˜+KšœžŸ œ9˜]Kšœ>‘œθ™§šžœ5žœ˜?šžœžœžœ˜#Jš‘œ5™7šžœ+žœž˜;šžœžœž˜˜šžœžœžœžœ˜?Kšœžœ8˜BKšžœ˜K˜——Kšžœ˜—šž˜Kšœžœ˜šžœ˜ Kšœžœžœ˜<——Kšžœ˜—K˜—K˜—šžœ˜Kš‘œ5™7KšœžœH˜aKšœ˜—Kšžœžœ˜KšœŸ˜K˜—Kšœžœ˜K˜CKšžœ?˜EK˜K˜K˜K˜—š  œžœžœžœžœ˜Lš œžœžœžœ˜NKšœ™K˜+Jšœ(‘œA™kšžœ+žœž˜;šžœžœž˜˜š žœžœžœžœžœžœž˜gKšœžœ8˜B——Kšžœ˜—Kšžœ˜—Kšžœžœ˜K˜K˜—Kšœžœ˜K˜CKšžœ˜ K˜K˜—š œžœžœ˜PKšœ™šžœ+žœž˜;šžœžœž˜˜Kšžœžœžœ˜A—Kšžœ˜—Kšžœ˜—Kšžœ ˜K˜—K˜—˜Lšœ™K˜Kšœžœžœ˜2šœžœžœ˜#K˜Kš œžœžŸžœžœ˜,Kšœ žœžœ˜Kšœžœž˜K˜—Kš œžœžœžŸžœžœ˜=K˜š œžœ žœ˜,Kšžœžœ˜2Kšœ·™·˜3K˜K˜—š œžœžœžœ˜NKšœS™SKšžœžœžœžœ˜2šžœ˜šžœžœžœ˜*Kšœžœ/žœ˜SKšœžœ˜K˜—Kšžœžœ˜K˜—K˜—š œžœžœžœ˜OKšœT™TKšžœžœžœžœ˜2šžœ˜K˜7K˜+šžœ+žœž˜<šžœžœž˜˜šžœžœžœžœ˜?K˜8š žœ žœžœžœ#ž˜?Kšœžœ&˜<—K˜——Kšžœ˜—Kšžœ˜—Kšžœžœ˜K˜—K˜—Kšœžœ˜˜K˜#K˜-K˜0—Kšžœ˜K˜——˜šœ žœžœ˜Kšœžœ˜š œ žœ žœžœžœ˜-K˜!Kšœ žœžŸžœž˜.K˜—K˜—Kšœžœžœ ˜K˜š  œžœ%žœ˜LKšœ„™„Kš œžœžœžœžœ˜Kšœ žœ˜K˜Kšœžœ˜$š œžœ žœžœ!žœ˜HKšžœžœ˜&—šž˜šœŸ˜Kšœžœ ˜Kšžœ žœžœ˜*šž˜Kšœ,™,K˜6Kšœžœ˜šž˜Kšœ?™?Kš œžœžœžœžœ˜-šžœžœžœ˜Kšœ)™)Kšœ%žœ˜+Kšžœžœžœ˜-K˜K˜—šžœ˜K˜!Kšœžœ ˜Kšžœ žœžœ ˜!šžœžœ žœ˜Kšœ™K˜Kšžœ˜K˜—K˜—Kšžœ˜—Kšžœ˜—šž˜šœžœžœžœ˜6K˜Kšžœ˜ K˜—˜KšœL™LKšœ žœ˜šžœ˜ Kšžœ#žœžœ˜3—šžœžœžœž˜)Kšœ,žœ˜1Kšžœ˜—K˜$Kšžœ˜ K˜——K˜—Kšžœ˜—šžœ˜ šžœžœžœ!ž˜0Kšœ žœžœ˜Kšžœ˜——K˜——˜š  œžœ žœ˜@Kšœ<™