source: trunk/zpush/lib/core/loopdetection.php @ 7589

Revision 7589, 36.6 KB checked in by douglas, 11 years ago (diff)

Ticket #3209 - Integrar módulo de sincronização Z-push ao Expresso

Line 
1<?php
2/***********************************************
3* File      :   loopdetection.php
4* Project   :   Z-Push
5* Descr     :   detects an outgoing loop by looking
6*               if subsequent requests do try to get changes
7*               for the same sync key. If more than once a synckey
8*               is requested, the amount of items to be sent to the mobile
9*               is reduced to one. If then (again) the same synckey is
10*               requested, we have most probably found the 'broken' item.
11*
12* Created   :   20.10.2011
13*
14* Copyright 2007 - 2012 Zarafa Deutschland GmbH
15*
16* This program is free software: you can redistribute it and/or modify
17* it under the terms of the GNU Affero General Public License, version 3,
18* as published by the Free Software Foundation with the following additional
19* term according to sec. 7:
20*
21* According to sec. 7 of the GNU Affero General Public License, version 3,
22* the terms of the AGPL are supplemented with the following terms:
23*
24* "Zarafa" is a registered trademark of Zarafa B.V.
25* "Z-Push" is a registered trademark of Zarafa Deutschland GmbH
26* The licensing of the Program under the AGPL does not imply a trademark license.
27* Therefore any rights, title and interest in our trademarks remain entirely with us.
28*
29* However, if you propagate an unmodified version of the Program you are
30* allowed to use the term "Z-Push" to indicate that you distribute the Program.
31* Furthermore you may use our trademarks where it is necessary to indicate
32* the intended purpose of a product or service provided you use it in accordance
33* with honest practices in industrial or commercial matters.
34* If you want to propagate modified versions of the Program under the name "Z-Push",
35* you may only do so if you have a written permission by Zarafa Deutschland GmbH
36* (to acquire a permission please contact Zarafa at trademark@zarafa.com).
37*
38* This program is distributed in the hope that it will be useful,
39* but WITHOUT ANY WARRANTY; without even the implied warranty of
40* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
41* GNU Affero General Public License for more details.
42*
43* You should have received a copy of the GNU Affero General Public License
44* along with this program.  If not, see <http://www.gnu.org/licenses/>.
45*
46* Consult LICENSE file for details
47************************************************/
48
49
50class LoopDetection extends InterProcessData {
51    const INTERPROCESSLD = "ipldkey";
52    const BROKENMSGS = "bromsgs";
53    static private $processident;
54    static private $processentry;
55    private $ignore_messageid;
56    private $broken_message_uuid;
57    private $broken_message_counter;
58
59
60    /**
61     * Constructor
62     *
63     * @access public
64     */
65    public function LoopDetection() {
66        // initialize super parameters
67        $this->allocate = 1024000; // 1 MB
68        $this->type = 1337;
69        parent::__construct();
70
71        $this->ignore_messageid = false;
72    }
73
74    /**
75     * PROCESS LOOP DETECTION
76     */
77
78    /**
79     * Adds the process entry to the process stack
80     *
81     * @access public
82     * @return boolean
83     */
84    public function ProcessLoopDetectionInit() {
85        return $this->updateProcessStack();
86    }
87
88    /**
89     * Marks the process entry as termineted successfully on the process stack
90     *
91     * @access public
92     * @return boolean
93     */
94    public function ProcessLoopDetectionTerminate() {
95        // just to be sure that the entry is there
96        self::GetProcessEntry();
97
98        self::$processentry['end'] = time();
99        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->ProcessLoopDetectionTerminate()");
100        return $this->updateProcessStack();
101    }
102
103    /**
104     * Returns a unique identifier for the internal process tracking
105     *
106     * @access public
107     * @return string
108     */
109    public static function GetProcessIdentifier() {
110        if (!isset(self::$processident))
111            self::$processident = sprintf('%04x%04', mt_rand(0, 0xffff), mt_rand(0, 0xffff));
112
113        return self::$processident;
114    }
115
116    /**
117     * Returns a unique entry with informations about the current process
118     *
119     * @access public
120     * @return array
121     */
122    public static function GetProcessEntry() {
123        if (!isset(self::$processentry)) {
124            self::$processentry = array();
125            self::$processentry['id'] = self::GetProcessIdentifier();
126            self::$processentry['pid'] = self::$pid;
127            self::$processentry['time'] = self::$start;
128            self::$processentry['cc'] = Request::GetCommandCode();
129        }
130
131        return self::$processentry;
132    }
133
134    /**
135     * Adds an Exceptions to the process tracking
136     *
137     * @param Exception     $exception
138     *
139     * @access public
140     * @return boolean
141     */
142    public function ProcessLoopDetectionAddException($exception) {
143        // generate entry if not already there
144        self::GetProcessEntry();
145
146        if (!isset(self::$processentry['stat']))
147            self::$processentry['stat'] = array();
148
149        self::$processentry['stat'][get_class($exception)] = $exception->getCode();
150
151        $this->updateProcessStack();
152        return true;
153    }
154
155    /**
156     * Adds a folderid and connected status code to the process tracking
157     *
158     * @param string    $folderid
159     * @param int       $status
160     *
161     * @access public
162     * @return boolean
163     */
164    public function ProcessLoopDetectionAddStatus($folderid, $status) {
165        // generate entry if not already there
166        self::GetProcessEntry();
167
168        if ($folderid === false)
169            $folderid = "hierarchy";
170
171        if (!isset(self::$processentry['stat']))
172            self::$processentry['stat'] = array();
173
174        self::$processentry['stat'][$folderid] = $status;
175
176        $this->updateProcessStack();
177
178        return true;
179    }
180
181    /**
182     * Indicates if a full Hierarchy Resync is necessary
183     *
184     * In some occasions the mobile tries to sync a folder with an invalid/not-existing ID.
185     * In these cases a status exception like SYNC_STATUS_FOLDERHIERARCHYCHANGED is returned
186     * so the mobile executes a FolderSync expecting that some action is taken on that folder (e.g. remove).
187     *
188     * If the FolderSync is not doing anything relevant, then the Sync is attempted again
189     * resulting in the same error and looping between these two processes.
190     *
191     * This method checks if in the last process stack a Sync and FolderSync were triggered to
192     * catch the loop at the 2nd interaction (Sync->FolderSync->Sync->FolderSync => ReSync)
193     * Ticket: https://jira.zarafa.com/browse/ZP-5
194     *
195     * @access public
196     * @return boolean
197     *
198     */
199    public function ProcessLoopDetectionIsHierarchyResyncRequired() {
200        $seenFailed = array();
201        $seenFolderSync = false;
202
203        $lookback = self::$start - 600; // look at the last 5 min
204        foreach ($this->getProcessStack() as $se) {
205            if ($se['time'] > $lookback && $se['time'] < (self::$start-1)) {
206                // look for sync command
207                if (isset($se['stat']) && ($se['cc'] == ZPush::COMMAND_SYNC || $se['cc'] == ZPush::COMMAND_PING)) {
208                    foreach($se['stat'] as $key => $value) {
209                        if (!isset($seenFailed[$key]))
210                            $seenFailed[$key] = 0;
211                        $seenFailed[$key]++;
212                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): seen command with Exception or folderid '%s' and code '%s'", $key, $value ));
213                    }
214                }
215                // look for FolderSync command with previous failed commands
216                if ($se['cc'] == ZPush::COMMAND_FOLDERSYNC && !empty($seenFailed) && $se['id'] != self::GetProcessIdentifier()) {
217                    // a full folderresync was already triggered
218                    if (isset($se['stat']) && isset($se['stat']['hierarchy']) && $se['stat']['hierarchy'] == SYNC_FSSTATUS_SYNCKEYERROR) {
219                        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): a full FolderReSync was already requested. Resetting fail counter.");
220                        $seenFailed = array();
221                    }
222                    else {
223                        $seenFolderSync = true;
224                        if (!empty($seenFailed))
225                            ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): seen FolderSync after other failing command");
226                    }
227                }
228            }
229        }
230
231        $filtered = array();
232        foreach ($seenFailed as $k => $count) {
233            if ($count>1)
234                $filtered[] = $k;
235        }
236
237        if ($seenFolderSync && !empty($filtered)) {
238            ZLog::Write(LOGLEVEL_INFO, "LoopDetection->ProcessLoopDetectionIsHierarchyResyncRequired(): Potential loop detected. Full hierarchysync indicated.");
239            return true;
240        }
241
242        return false;
243    }
244
245    /**
246     * Indicates if a previous process could not be terminated
247     *
248     * Checks if there is an end time for the last entry on the stack
249     *
250     * @access public
251     * @return boolean
252     *
253     */
254    public function ProcessLoopDetectionPreviousConnectionFailed() {
255        $stack = $this->getProcessStack();
256        if (count($stack) > 1) {
257            $se = $stack[0];
258            if (!isset($se['end']) && $se['cc'] != ZPush::COMMAND_PING) {
259                // there is no end time
260                ZLog::Write(LOGLEVEL_ERROR, sprintf("LoopDetection->ProcessLoopDetectionPreviousConnectionFailed(): Command '%s' at %s with pid '%d' terminated unexpectedly or is still running.", Utils::GetCommandFromCode($se['cc']), Utils::GetFormattedTime($se['time']), $se['pid']));
261                ZLog::Write(LOGLEVEL_ERROR, "Please check your logs for this PID and errors like PHP-Fatals or Apache segmentation faults and report your results to the Z-Push dev team.");
262            }
263        }
264    }
265
266    /**
267     * Gets the PID of an outdated search process
268     *
269     * Returns false if there isn't any process
270     *
271     * @access public
272     * @return boolean
273     *
274     */
275    public function ProcessLoopDetectionGetOutdatedSearchPID() {
276        $stack = $this->getProcessStack();
277        if (count($stack) > 1) {
278            $se = $stack[0];
279            if ($se['cc'] == ZPush::COMMAND_SEARCH) {
280                return $se['pid'];
281            }
282        }
283        return false;
284    }
285
286    /**
287     * Inserts or updates the current process entry on the stack
288     *
289     * @access private
290     * @return boolean
291     */
292    private function updateProcessStack() {
293        // initialize params
294        $this->InitializeParams();
295        if ($this->blockMutex()) {
296            $loopdata = ($this->hasData()) ? $this->getData() : array();
297
298            // check and initialize the array structure
299            $this->checkArrayStructure($loopdata, self::INTERPROCESSLD);
300
301            $stack = $loopdata[self::$devid][self::$user][self::INTERPROCESSLD];
302
303            // insert/update current process entry
304            $nstack = array();
305            $updateentry = self::GetProcessEntry();
306            $found = false;
307
308            foreach ($stack as $entry) {
309                if ($entry['id'] != $updateentry['id']) {
310                    $nstack[] = $entry;
311                }
312                else {
313                    $nstack[] = $updateentry;
314                    $found = true;
315                }
316            }
317
318            if (!$found)
319                $nstack[] = $updateentry;
320
321            if (count($nstack) > 10)
322                $nstack = array_slice($nstack, -10, 10);
323
324            // update loop data
325            $loopdata[self::$devid][self::$user][self::INTERPROCESSLD] = $nstack;
326            $ok = $this->setData($loopdata);
327
328            $this->releaseMutex();
329        }
330        // end exclusive block
331
332        return true;
333    }
334
335    /**
336     * Returns the current process stack
337     *
338     * @access private
339     * @return array
340     */
341    private function getProcessStack() {
342        // initialize params
343        $this->InitializeParams();
344        $stack = array();
345
346        if ($this->blockMutex()) {
347            $loopdata = ($this->hasData()) ? $this->getData() : array();
348
349            // check and initialize the array structure
350            $this->checkArrayStructure($loopdata, self::INTERPROCESSLD);
351
352            $stack = $loopdata[self::$devid][self::$user][self::INTERPROCESSLD];
353
354            $this->releaseMutex();
355        }
356        // end exclusive block
357
358        return $stack;
359    }
360
361    /**
362     * TRACKING OF BROKEN MESSAGES
363     * if a previousily ignored message is streamed again to the device it's tracked here
364     *
365     * There are two outcomes:
366     * - next uuid counter is higher than current -> message is fixed and successfully synchronized
367     * - next uuid counter is the same or uuid changed -> message is still broken
368     */
369
370    /**
371     * Adds a message to the tracking of broken messages
372     * Being tracked means that a broken message was streamed to the device.
373     * We save the latest uuid and counter so if on the next sync the counter is higher
374     * the message was accepted by the device.
375     *
376     * @param string    $folderid   the parent folder of the message
377     * @param string    $id         the id of the message
378     *
379     * @access public
380     * @return boolean
381     */
382    public function SetBrokenMessage($folderid, $id) {
383        if ($folderid == false || !isset($this->broken_message_uuid) || !isset($this->broken_message_counter) || $this->broken_message_uuid == false || $this->broken_message_counter == false)
384            return false;
385
386        $ok = false;
387        $brokenkey = self::BROKENMSGS ."-". $folderid;
388
389        // initialize params
390        $this->InitializeParams();
391        if ($this->blockMutex()) {
392            $loopdata = ($this->hasData()) ? $this->getData() : array();
393
394            // check and initialize the array structure
395            $this->checkArrayStructure($loopdata, $brokenkey);
396
397            $brokenmsgs = $loopdata[self::$devid][self::$user][$brokenkey];
398
399            $brokenmsgs[$id] = array('uuid' => $this->broken_message_uuid, 'counter' => $this->broken_message_counter);
400            ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->SetBrokenMessage('%s', '%s'): tracking broken message", $folderid, $id));
401
402            // update data
403            $loopdata[self::$devid][self::$user][$brokenkey] = $brokenmsgs;
404            $ok = $this->setData($loopdata);
405
406            $this->releaseMutex();
407        }
408        // end exclusive block
409
410        return $ok;
411    }
412
413    /**
414     * Gets a list of all ids of a folder which were tracked and which were
415     * accepted by the device from the last sync.
416     *
417     * @param string    $folderid   the parent folder of the message
418     * @param string    $id         the id of the message
419     *
420     * @access public
421     * @return array
422     */
423    public function GetSyncedButBeforeIgnoredMessages($folderid) {
424        if ($folderid == false || !isset($this->broken_message_uuid) || !isset($this->broken_message_counter) || $this->broken_message_uuid == false || $this->broken_message_counter == false)
425            return array();
426
427        $brokenkey = self::BROKENMSGS ."-". $folderid;
428        $removeIds = array();
429        $okIds = array();
430
431        // initialize params
432        $this->InitializeParams();
433        if ($this->blockMutex()) {
434            $loopdata = ($this->hasData()) ? $this->getData() : array();
435
436            // check and initialize the array structure
437            $this->checkArrayStructure($loopdata, $brokenkey);
438
439            $brokenmsgs = $loopdata[self::$devid][self::$user][$brokenkey];
440
441            if (!empty($brokenmsgs)) {
442                foreach ($brokenmsgs as $id => $data) {
443                    // previously broken message was sucessfully synced!
444                    if ($data['uuid'] == $this->broken_message_uuid && $data['counter'] < $this->broken_message_counter) {
445                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->GetSyncedButBeforeIgnoredMessages('%s'): message '%s' was successfully synchronized", $folderid, $id));
446                        $okIds[] = $id;
447                    }
448
449                    // if the uuid has changed this is old data which should also be removed
450                    if ($data['uuid'] != $this->broken_message_uuid) {
451                        ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->GetSyncedButBeforeIgnoredMessages('%s'): stored message id '%s' for uuid '%s' is obsolete", $folderid, $id, $data['uuid']));
452                        $removeIds[] = $id;
453                    }
454                }
455
456                // remove data
457                foreach (array_merge($okIds,$removeIds) as $id) {
458                    unset($brokenmsgs[$id]);
459                }
460
461                if (empty($brokenmsgs) && isset($loopdata[self::$devid][self::$user][$brokenkey])) {
462                    unset($loopdata[self::$devid][self::$user][$brokenkey]);
463                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->GetSyncedButBeforeIgnoredMessages('%s'): removed folder from tracking of ignored messages", $folderid));
464                }
465                else {
466                    // update data
467                    $loopdata[self::$devid][self::$user][$brokenkey] = $brokenmsgs;
468                }
469                $ok = $this->setData($loopdata);
470            }
471
472            $this->releaseMutex();
473        }
474        // end exclusive block
475
476        return $okIds;
477    }
478
479    /**
480     * Marks a SyncState as "already used", e.g. when an import process started.
481     * This is most critical for DiffBackends, as an imported message would be exported again
482     * in the heartbeat if the notification is triggered before the import is complete.
483     *
484     * @param string $folderid          folder id
485     * @param string $uuid              synkkey
486     * @param string $counter           synckey counter
487     *
488     * @access public
489     * @return boolean
490     */
491    public function SetSyncStateUsage($folderid, $uuid, $counter) {
492        // initialize params
493        $this->InitializeParams();
494
495        ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->SetSyncStateUsage(): uuid: %s  counter: %d", $uuid, $counter));
496
497        // exclusive block
498        if ($this->blockMutex()) {
499            $loopdata = ($this->hasData()) ? $this->getData() : array();
500            // check and initialize the array structure
501            $this->checkArrayStructure($loopdata, $folderid);
502            $current = $loopdata[self::$devid][self::$user][$folderid];
503
504            // update the usage flag
505            $current["usage"] = $counter;
506
507            // update loop data
508            $loopdata[self::$devid][self::$user][$folderid] = $current;
509            $ok = $this->setData($loopdata);
510
511            $this->releaseMutex();
512        }
513        // end exclusive block
514    }
515
516    /**
517     * Checks if the given counter for a certain uuid+folderid was exported before.
518     * Returns also true if the counter are the same but previously there were
519     * changes to be exported.
520     *
521     * @param string $folderid          folder id
522     * @param string $uuid              synkkey
523     * @param string $counter           synckey counter
524     *
525     * @access public
526     * @return boolean                  indicating if an uuid+counter were exported (with changes) before
527     */
528    public function IsSyncStateObsolete($folderid, $uuid, $counter) {
529        // initialize params
530        $this->InitializeParams();
531
532        $obsolete = false;
533
534        // exclusive block
535        if ($this->blockMutex()) {
536            $loopdata = ($this->hasData()) ? $this->getData() : array();
537            $this->releaseMutex();
538            // end exclusive block
539
540            // check and initialize the array structure
541            $this->checkArrayStructure($loopdata, $folderid);
542
543            $current = $loopdata[self::$devid][self::$user][$folderid];
544
545            if (!empty($current)) {
546                if ($current["uuid"] != $uuid) {
547                    ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->IsSyncStateObsolete(): yes, uuid changed");
548                    $obsolete = true;
549                }
550                ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->IsSyncStateObsolete(): check uuid counter: %d - last known counter: %d with %d queued objects", $counter, $current["count"], $current["queued"]));
551
552                if ($current["uuid"] == $uuid && ($current["count"] > $counter || ($current["count"] == $counter && $current["queued"] > 0) || (isset($current["usage"]) && $current["usage"] >= $counter))) {
553                    $usage = isset($current["usage"]) ? sprintf(" - counter %d already expired",$current["usage"]) : "";
554                    ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->IsSyncStateObsolete(): yes, counter already processed". $usage);
555                    $obsolete = true;
556                }
557            }
558        }
559
560        return $obsolete;
561    }
562
563    /**
564     * MESSAGE LOOP DETECTION
565     */
566
567    /**
568     * Loop detection mechanism
569     *
570     *    1. request counter is higher than the previous counter (somehow default)
571     *      1.1)   standard situation                                   -> do nothing
572     *      1.2)   loop information exists
573     *      1.2.1) request counter < maxCounter AND no ignored data     -> continue in loop mode
574     *      1.2.2) request counter < maxCounter AND ignored data        -> we have already encountered issue, return to normal
575     *
576     *    2. request counter is the same as the previous, but no data was sent on the last request (standard situation)
577     *
578     *    3. request counter is the same as the previous and last time objects were sent (loop!)
579     *      3.1)   no loop was detected before, entereing loop mode     -> save loop data, loopcount = 1
580     *      3.2)   loop was detected before, but are gone               -> loop resolved
581     *      3.3)   loop was detected before, continuing in loop mode    -> this is probably the broken element,loopcount++,
582     *      3.3.1) item identified, loopcount >= 3                      -> ignore item, set ignoredata flag
583     *
584     * @param string $folderid          the current folder id to be worked on
585     * @param string $type              the type of that folder (Email, Calendar, Contact, Task)
586     * @param string $uuid              the synkkey
587     * @param string $counter           the synckey counter
588     * @param string $maxItems          the current amount of items to be sent to the mobile
589     * @param string $queuedMessages    the amount of messages which were found by the exporter
590     *
591     * @access public
592     * @return boolean      when returning true if a loop has been identified
593     */
594    public function Detect($folderid, $type, $uuid, $counter, $maxItems, $queuedMessages) {
595        $this->broken_message_uuid = $uuid;
596        $this->broken_message_counter = $counter;
597
598        // if an incoming loop is already detected, do nothing
599        if ($maxItems === 0 && $queuedMessages > 0) {
600            ZPush::GetTopCollector()->AnnounceInformation("Incoming loop!", true);
601            return true;
602        }
603
604        // initialize params
605        $this->InitializeParams();
606
607        $loop = false;
608
609        // exclusive block
610        if ($this->blockMutex()) {
611            $loopdata = ($this->hasData()) ? $this->getData() : array();
612
613            // check and initialize the array structure
614            $this->checkArrayStructure($loopdata, $folderid);
615
616            $current = $loopdata[self::$devid][self::$user][$folderid];
617
618            // completely new/unknown UUID
619            if (empty($current))
620                $current = array("type" => $type, "uuid" => $uuid, "count" => $counter-1, "queued" => $queuedMessages);
621
622            // old UUID in cache - the device requested a new state!!
623            else if (isset($current['type']) && $current['type'] == $type && isset($current['uuid']) && $current['uuid'] != $uuid ) {
624                ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): UUID changed for folder");
625
626                // some devices (iPhones) may request new UUIDs after broken items were sent several times
627                if (isset($current['queued']) && $current['queued'] > 0 &&
628                    (isset($current['maxCount']) && $current['count']+1 < $current['maxCount'] || $counter == 1)) {
629
630                    ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): UUID changed and while items where sent to device - forcing loop mode");
631                    $loop = true; // force loop mode
632                    $current['queued'] = $queuedMessages;
633                }
634                else {
635                    $current['queued'] = 0;
636                }
637
638                // set new data, unset old loop information
639                $current["uuid"] = $uuid;
640                $current['count'] = $counter;
641                unset($current['loopcount']);
642                unset($current['ignored']);
643                unset($current['maxCount']);
644                unset($current['potential']);
645            }
646
647            // see if there are values
648            if (isset($current['uuid']) && $current['uuid'] == $uuid &&
649                isset($current['type']) && $current['type'] == $type &&
650                isset($current['count'])) {
651
652                // case 1 - standard, during loop-resolving & resolving
653                if ($current['count'] < $counter) {
654
655                    // case 1.1
656                    $current['count'] = $counter;
657                    $current['queued'] = $queuedMessages;
658                    if (isset($current["usage"]) && $current["usage"] < $current['count'])
659                        unset($current["usage"]);
660
661                    // case 1.2
662                    if (isset($current['maxCount'])) {
663                        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 1.2 detected");
664
665                        // case 1.2.1
666                        // broken item not identified yet
667                        if (!isset($current['ignored']) && $counter < $current['maxCount']) {
668                            $loop = true; // continue in loop-resolving
669                            ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 1.2.1 detected");
670                        }
671                        // case 1.2.2 - if there were any broken items they should be gone, return to normal
672                        else {
673                            ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 1.2.2 detected");
674                            unset($current['loopcount']);
675                            unset($current['ignored']);
676                            unset($current['maxCount']);
677                            unset($current['potential']);
678                        }
679                    }
680                }
681
682                // case 2 - same counter, but there were no changes before and are there now
683                else if ($current['count'] == $counter && $current['queued'] == 0 && $queuedMessages > 0) {
684                    $current['queued'] = $queuedMessages;
685                    if (isset($current["usage"]) && $current["usage"] < $current['count'])
686                        unset($current["usage"]);
687                }
688
689                // case 3 - same counter, changes sent before, hanging loop and ignoring
690                else if ($current['count'] == $counter && $current['queued'] > 0) {
691
692                    if (!isset($current['loopcount'])) {
693                        // case 3.1) we have just encountered a loop!
694                        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 3.1 detected - loop detected, init loop mode");
695                        $current['loopcount'] = 1;
696                        // the MaxCount is the max number of messages exported before
697                        $current['maxCount'] = $counter + (($maxItems < $queuedMessages)? $maxItems: $queuedMessages);
698                        $loop = true;   // loop mode!!
699                    }
700                    else if ($queuedMessages == 0) {
701                        // case 3.2) there was a loop before but now the changes are GONE
702                        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 3.2 detected - changes gone - clearing loop data");
703                        $current['queued'] = 0;
704                        unset($current['loopcount']);
705                        unset($current['ignored']);
706                        unset($current['maxCount']);
707                        unset($current['potential']);
708                    }
709                    else {
710                        // case 3.3) still looping the same message! Increase counter
711                        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->Detect(): case 3.3 detected - in loop mode, increase loop counter");
712                        $current['loopcount']++;
713
714                        // case 3.3.1 - we got our broken item!
715                        if ($current['loopcount'] >= 3 && isset($current['potential'])) {
716                            ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->Detect(): case 3.3.1 detected - broken item should be next, attempt to ignore it - id '%s'", $current['potential']));
717                            $this->ignore_messageid = $current['potential'];
718                        }
719                        $current['maxCount'] = $counter + $queuedMessages;
720                        $loop = true;   // loop mode!!
721                    }
722                }
723
724            }
725            if (isset($current['loopcount']))
726                ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->Detect(): loop data: loopcount(%d), maxCount(%d), queued(%d), ignored(%s)", $current['loopcount'], $current['maxCount'], $current['queued'], (isset($current['ignored'])?$current['ignored']:'false')));
727
728            // update loop data
729            $loopdata[self::$devid][self::$user][$folderid] = $current;
730            $ok = $this->setData($loopdata);
731
732            $this->releaseMutex();
733        }
734        // end exclusive block
735
736        if ($loop == true && $this->ignore_messageid == false) {
737            ZPush::GetTopCollector()->AnnounceInformation("Loop detection", true);
738        }
739
740        return $loop;
741    }
742
743    /**
744     * Indicates if the next messages should be ignored (not be sent to the mobile!)
745     *
746     * @param string  $messageid        (opt) id of the message which is to be exported next
747     * @param string  $folderid         (opt) parent id of the message
748     * @param boolean $markAsIgnored    (opt) to peek without setting the next message to be
749     *                                  ignored, set this value to false
750     * @access public
751     * @return boolean
752     */
753    public function IgnoreNextMessage($markAsIgnored = true, $messageid = false, $folderid = false) {
754        // as the next message id is not available at all point this method is called, we use different indicators.
755        // potentialbroken indicates that we know that the broken message should be exported next,
756        // alltho we do not know for sure as it's export message orders can change
757        // if the $messageid is available and matches then we are sure and only then really ignore it
758
759        $potentialBroken = false;
760        $realBroken = false;
761        if (Request::GetCommandCode() == ZPush::COMMAND_SYNC && $this->ignore_messageid !== false)
762            $potentialBroken = true;
763
764        if ($messageid !== false && $this->ignore_messageid == $messageid)
765            $realBroken = true;
766
767        // this call is just to know what should be happening
768        // no further actions necessary
769        if ($markAsIgnored === false) {
770            return $potentialBroken;
771        }
772
773        // we should really do something here
774
775        // first we check if we are in the loop mode, if so,
776        // we update the potential broken id message so we loop count the same message
777
778        $changedData = false;
779        // exclusive block
780        if ($this->blockMutex()) {
781            $loopdata = ($this->hasData()) ? $this->getData() : array();
782
783            // check and initialize the array structure
784            $this->checkArrayStructure($loopdata, $folderid);
785
786            $current = $loopdata[self::$devid][self::$user][$folderid];
787
788            // we found our broken message!
789            if ($realBroken) {
790                $this->ignore_messageid = false;
791                $current['ignored'] = $messageid;
792                $changedData = true;
793
794                // check if this message was broken before - here we know that it still is and remove it from the tracking
795                $brokenkey = self::BROKENMSGS ."-". $folderid;
796                if (isset($loopdata[self::$devid][self::$user][$brokenkey]) && isset($loopdata[self::$devid][self::$user][$brokenkey][$messageid])) {
797                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->IgnoreNextMessage(): previously broken message '%s' is still broken and will not be tracked anymore", $messageid));
798                    unset($loopdata[self::$devid][self::$user][$brokenkey][$messageid]);
799                }
800            }
801            // not the broken message yet
802            else {
803                // update potential id if looping on an item
804                if (isset($current['loopcount'])) {
805                    $current['potential'] = $messageid;
806
807                    // this message should be the broken one, but is not!!
808                    // we should reset the loop count because this is certainly not the broken one
809                    if ($potentialBroken) {
810                        $current['loopcount'] = 1;
811                        ZLog::Write(LOGLEVEL_DEBUG, "LoopDetection->IgnoreNextMessage(): this should be the broken one, but is not! Resetting loop count.");
812                    }
813
814                    ZLog::Write(LOGLEVEL_DEBUG, sprintf("LoopDetection->IgnoreNextMessage(): Loop mode, potential broken message id '%s'", $current['potential']));
815
816                    $changedData = true;
817                }
818            }
819
820            // update loop data
821            if ($changedData == true) {
822                $loopdata[self::$devid][self::$user][$folderid] = $current;
823                $ok = $this->setData($loopdata);
824            }
825
826            $this->releaseMutex();
827        }
828        // end exclusive block
829
830        if ($realBroken)
831            ZPush::GetTopCollector()->AnnounceInformation("Broken message ignored", true);
832
833        return $realBroken;
834    }
835
836    /**
837     * Clears loop detection data
838     *
839     * @param string    $user           (opt) user which data should be removed - user can not be specified without
840     * @param string    $devid          (opt) device id which data to be removed
841     *
842     * @return boolean
843     * @access public
844     */
845    public function ClearData($user = false, $devid = false) {
846        $stat = true;
847        $ok = false;
848
849        // exclusive block
850        if ($this->blockMutex()) {
851            $loopdata = ($this->hasData()) ? $this->getData() : array();
852
853            if ($user == false && $devid == false)
854                $loopdata = array();
855            elseif ($user == false && $devid != false)
856                $loopdata[$devid] = array();
857            elseif ($user != false && $devid != false)
858                $loopdata[$devid][$user] = array();
859            elseif ($user != false && $devid == false) {
860                ZLog::Write(LOGLEVEL_WARN, sprintf("Not possible to reset loop detection data for user '%s' without a specifying a device id", $user));
861                $stat = false;
862            }
863
864            if ($stat)
865                $ok = $this->setData($loopdata);
866
867            $this->releaseMutex();
868        }
869        // end exclusive block
870
871        return $stat && $ok;
872    }
873
874    /**
875     * Returns loop detection data for a user and device
876     *
877     * @param string    $user
878     * @param string    $devid
879     *
880     * @return array/boolean    returns false if data not available
881     * @access public
882     */
883    public function GetCachedData($user, $devid) {
884        // exclusive block
885        if ($this->blockMutex()) {
886            $loopdata = ($this->hasData()) ? $this->getData() : array();
887            $this->releaseMutex();
888        }
889        // end exclusive block
890        if (isset($loopdata) && isset($loopdata[$devid]) && isset($loopdata[$devid][$user]))
891            return $loopdata[$devid][$user];
892
893        return false;
894    }
895
896    /**
897     * Builds an array structure for the loop detection data
898     *
899     * @param array $loopdata    reference to the topdata array
900     *
901     * @access private
902     * @return
903     */
904    private function checkArrayStructure(&$loopdata, $folderid) {
905        if (!isset($loopdata) || !is_array($loopdata))
906            $loopdata = array();
907
908        if (!isset($loopdata[self::$devid]))
909            $loopdata[self::$devid] = array();
910
911        if (!isset($loopdata[self::$devid][self::$user]))
912            $loopdata[self::$devid][self::$user] = array();
913
914        if (!isset($loopdata[self::$devid][self::$user][$folderid]))
915            $loopdata[self::$devid][self::$user][$folderid] = array();
916    }
917}
918
919?>
Note: See TracBrowser for help on using the repository browser.