aboutsummaryrefslogtreecommitdiff
path: root/src/philo/philo.py
blob: 0e62e97be61499fef01bff372cb167c56198e093 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# ############################################################################ #
#                                                                              #
#                                                         :::      ::::::::    #
#    philo.py                                           :+:      :+:    :+:    #
#                                                     +:+ +:+         +:+      #
#    By: cacharle <me@cacharle.xyz>                 +#+  +:+       +#+         #
#                                                 +#+#+#+#+#+   +#+            #
#    Created: 2020/10/01 10:52:56 by cacharle          #+#    #+#              #
#    Updated: 2021/01/03 13:31:54 by cacharle         ###   ########.fr        #
#                                                                              #
# ############################################################################ #

import itertools

from philo import error
from philo.event import Event


class Philo:
    def __init__(
        self,
        id_:           int,
        timeout_die:   int,
        timeout_eat:   int,
        timeout_sleep: int,
        meal_num:      int
    ):
        self.logs          = []
        self.id             = id_
        self._timeout_die   = timeout_die
        self._timeout_eat   = timeout_eat
        self._timeout_sleep = timeout_sleep
        self._meal_num      = meal_num

    def check(self):
        """ Check log for errors

            - Must take 2 forks before eating
            - The delay between the taking of the second fork and eating should be almost 0
            - State switch should be
              thinking -> take fork -> take fork -> eat * meal_num -> sleep -> repeat
            - Should die when starving: last log timestamp - timeout_death > last_time_eat
            - Should eat n times, Should take fork 2 times, The other event should happend 1 time
        """

        if len(self.logs) == 0:
            return

        # check 2 forks before eating
        for l1, l2, l3 in zip(self.logs, self.logs[1:], self.logs[2:]):
            if (l3.event is Event.EAT
                and (l1.event is not Event.FORK
                     or l2.event is not Event.FORK)):
                self._raise("should take 2 forks then eat")

        # check log event number
        grouped = [(e, list(g)) for e, g in itertools.groupby(self.logs, (lambda x: x.event))]
        for e, g in grouped[:-1]:
            # if e is Event.EAT:
            #     if len(g) != self._meal_num:
            #         self._raise("Should eat {} times".format(self._meal_num))
            if e is Event.FORK:
                if len(g) != 2:
                    self._raise("Should take fork 2 times")
            elif len(g) != 1:
                self._raise("Event `{}` should occur 1 time".format(Event.to_string(e)))

        # check state switch order
        events = [e for e, _ in grouped]
        for e1, e2 in zip(events, events[1:]):
            second = {
                Event.THINK: Event.FORK,
                Event.FORK:  Event.EAT,
                Event.EAT:   Event.SLEEP,
                Event.SLEEP: Event.THINK
            }[e1]
            if e2 is not second:
                self._raise("invalid state switch `{}` -> `{}`".format(
                    Event.to_string(e1), Event.to_string(e2)))

        # check timeouts
        for l1, l2 in zip(self.logs, self.logs[1:]):
            e1, e2 = l1.event, l2.event
            t1, t2 = l1.timestamp, l2.timestamp
            if e1 is Event.FORK and e2 is Event.EAT:
                if t2 - t1 > 10:
                    self._raise("Delay between taking second fork and eat > 10")
            if e1 is Event.SLEEP:
                self._check_time_range(t1, t2, self._timeout_sleep, "Slept")
            if e1 is Event.EAT:
                self._check_time_range(t1, t2, self._timeout_eat, "Ate")

        # check if should be dead
        last_eat = next(
            (log for log in reversed(self.logs) if log.event is Event.EAT),
            None
        )
        last = self.logs[-1]
        if last_eat is not None and last_eat is not last:
            if last.timestamp - last_eat.timestamp > self._timeout_die + 10:
                self._raise(
                    "{} should be dead {} - {} > {}"
                    .format(self.id, last.timestamp,
                            last_eat.timestamp, self._timeout_die + 10)
                )

    def _check_time_range(self, t1, t2, timeout, verb):
        start = timeout - 10
        end = timeout + 10
        if not (start <= t2 - t1 <= end):
            self._raise("{} {}ms expected {}-{}ms".format(verb, t2 - t1, start, end))

    def _raise(self, msg):
        """ Helper to raise Log errrors"""
        raise error.Log(self.logs, msg)

    @property
    def used_forks(self):
        """ The number of forks currently used by the philosopher """
        if len(self.logs) < 1:
            return 0
        if self.logs[-1].event is Event.EAT:
            return 2
        if self.logs[-1].event is Event.FORK:
            if len(self.logs) > 1 and self.logs[-2].event is Event.FORK:
                return 2
            else:
                return 1
        return 0