prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  There's a small window where events will be missed by reporters during reconfig
  The code in `ReporterBase.reconfigService()` stops consuming events for all event keys and then starts consuming events for all event keys again. This is not necessary for any wanted event keys which are wanted in both old and new configuration. The code should stop consuming events for no longer wanted event keys, start consuming events for newly wanted event keys and leave the rest untouched.
  
  </issue>
  <code>
  [start of master/buildbot/reporters/base.py]
  1 # This file is part of Buildbot.  Buildbot is free software: you can
  2 # redistribute it and/or modify it under the terms of the GNU General Public
  3 # License as published by the Free Software Foundation, version 2.
  4 #
  5 # This program is distributed in the hope that it will be useful, but WITHOUT
  6 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  7 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
  8 # details.
  9 #
  10 # You should have received a copy of the GNU General Public License along with
  11 # this program; if not, write to the Free Software Foundation, Inc., 51
  12 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  13 #
  14 # Copyright Buildbot Team Members
  15 
  16 import abc
  17 
  18 from twisted.internet import defer
  19 from twisted.python import log
  20 
  21 from buildbot import config
  22 from buildbot.reporters import utils
  23 from buildbot.util import service
  24 from buildbot.util import tuplematch
  25 
  26 ENCODING = 'utf-8'
  27 
  28 
  29 class ReporterBase(service.BuildbotService):
  30     name = None
  31     __meta__ = abc.ABCMeta
  32 
  33     compare_attrs = ['generators']
  34 
  35     def __init__(self, *args, **kwargs):
  36         super().__init__(*args, **kwargs)
  37         self.generators = None
  38         self._event_consumers = []
  39         self._pending_got_event_calls = {}
  40 
  41     def checkConfig(self, generators):
  42         if not isinstance(generators, list):
  43             config.error('{}: generators argument must be a list')
  44 
  45         for g in generators:
  46             g.check()
  47 
  48         if self.name is None:
  49             self.name = self.__class__.__name__
  50             for g in generators:
  51                 self.name += "_" + g.generate_name()
  52 
  53     @defer.inlineCallbacks
  54     def reconfigService(self, generators):
  55 
  56         for consumer in self._event_consumers:
  57             yield consumer.stopConsuming()
  58         self._event_consumers = []
  59 
  60         self.generators = generators
  61 
  62         wanted_event_keys = set()
  63         for g in self.generators:
  64             wanted_event_keys.update(g.wanted_event_keys)
  65 
  66         for key in sorted(list(wanted_event_keys)):
  67             consumer = yield self.master.mq.startConsuming(self._got_event, key)
  68             self._event_consumers.append(consumer)
  69 
  70     @defer.inlineCallbacks
  71     def stopService(self):
  72         for consumer in self._event_consumers:
  73             yield consumer.stopConsuming()
  74         self._event_consumers = []
  75 
  76         for pending_call in list(self._pending_got_event_calls.values()):
  77             yield pending_call
  78         self._pending_got_event_calls = {}
  79 
  80         yield super().stopService()
  81 
  82     def _does_generator_want_key(self, generator, key):
  83         for filter in generator.wanted_event_keys:
  84             if tuplematch.matchTuple(key, filter):
  85                 return True
  86         return False
  87 
  88     def _get_chain_key_for_event(self, key, msg):
  89         if key[0] in ["builds", "buildrequests"]:
  90             return ("buildrequestid", msg["buildrequestid"])
  91         return None
  92 
  93     @defer.inlineCallbacks
  94     def _got_event(self, key, msg):
  95         chain_key = self._get_chain_key_for_event(key, msg)
  96         if chain_key is not None:
  97             d = defer.Deferred()
  98             pending_call = self._pending_got_event_calls.get(chain_key)
  99             self._pending_got_event_calls[chain_key] = d
  100             # Wait for previously pending call, if any, to ensure
  101             # reports are sent out in the order events were queued.
  102             if pending_call is not None:
  103                 yield pending_call
  104 
  105         try:
  106             reports = []
  107             for g in self.generators:
  108                 if self._does_generator_want_key(g, key):
  109                     report = yield g.generate(self.master, self, key, msg)
  110                     if report is not None:
  111                         reports.append(report)
  112 
  113             if reports:
  114                 yield self.sendMessage(reports)
  115         except Exception as e:
  116             log.err(e, 'Got exception when handling reporter events')
  117 
  118         if chain_key is not None:
  119             if self._pending_got_event_calls.get(chain_key) == d:
  120                 del self._pending_got_event_calls[chain_key]
  121             d.callback(None)  # This event is now fully handled
  122 
  123     def getResponsibleUsersForBuild(self, master, buildid):
  124         # Use library method but subclassers may want to override that
  125         return utils.getResponsibleUsersForBuild(master, buildid)
  126 
  127     @abc.abstractmethod
  128     def sendMessage(self, reports):
  129         pass
  130
  [end of master/buildbot/reporters/base.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/master/buildbot/reporters/base.py b/master/buildbot/reporters/base.py
  --- a/master/buildbot/reporters/base.py
  +++ b/master/buildbot/reporters/base.py
  @@ -35,7 +35,7 @@
       def __init__(self, *args, **kwargs):
           super().__init__(*args, **kwargs)
           self.generators = None
  -        self._event_consumers = []
  +        self._event_consumers = {}
           self._pending_got_event_calls = {}
   
       def checkConfig(self, generators):
  @@ -52,26 +52,29 @@
   
       @defer.inlineCallbacks
       def reconfigService(self, generators):
  -
  -        for consumer in self._event_consumers:
  -            yield consumer.stopConsuming()
  -        self._event_consumers = []
  -
           self.generators = generators
   
           wanted_event_keys = set()
           for g in self.generators:
               wanted_event_keys.update(g.wanted_event_keys)
   
  +        # Remove consumers for keys that are no longer wanted
  +        for key in list(self._event_consumers.keys()):
  +            if key not in wanted_event_keys:
  +                yield self._event_consumers[key].stopConsuming()
  +                del self._event_consumers[key]
  +
  +        # Add consumers for new keys
           for key in sorted(list(wanted_event_keys)):
  -            consumer = yield self.master.mq.startConsuming(self._got_event, key)
  -            self._event_consumers.append(consumer)
  +            if key not in self._event_consumers:
  +                self._event_consumers[key] = \
  +                    yield self.master.mq.startConsuming(self._got_event, key)
   
       @defer.inlineCallbacks
       def stopService(self):
  -        for consumer in self._event_consumers:
  +        for consumer in self._event_consumers.values():
               yield consumer.stopConsuming()
  -        self._event_consumers = []
  +        self._event_consumers = {}
   
           for pending_call in list(self._pending_got_event_calls.values()):
               yield pending_call
  
verification_info: {"golden_diff": "diff --git a/master/buildbot/reporters/base.py b/master/buildbot/reporters/base.py\n--- a/master/buildbot/reporters/base.py\n+++ b/master/buildbot/reporters/base.py\n@@ -35,7 +35,7 @@\n     def __init__(self, *args, **kwargs):\n         super().__init__(*args, **kwargs)\n         self.generators = None\n-        self._event_consumers = []\n+        self._event_consumers = {}\n         self._pending_got_event_calls = {}\n \n     def checkConfig(self, generators):\n@@ -52,26 +52,29 @@\n \n     @defer.inlineCallbacks\n     def reconfigService(self, generators):\n-\n-        for consumer in self._event_consumers:\n-            yield consumer.stopConsuming()\n-        self._event_consumers = []\n-\n         self.generators = generators\n \n         wanted_event_keys = set()\n         for g in self.generators:\n             wanted_event_keys.update(g.wanted_event_keys)\n \n+        # Remove consumers for keys that are no longer wanted\n+        for key in list(self._event_consumers.keys()):\n+            if key not in wanted_event_keys:\n+                yield self._event_consumers[key].stopConsuming()\n+                del self._event_consumers[key]\n+\n+        # Add consumers for new keys\n         for key in sorted(list(wanted_event_keys)):\n-            consumer = yield self.master.mq.startConsuming(self._got_event, key)\n-            self._event_consumers.append(consumer)\n+            if key not in self._event_consumers:\n+                self._event_consumers[key] = \\\n+                    yield self.master.mq.startConsuming(self._got_event, key)\n \n     @defer.inlineCallbacks\n     def stopService(self):\n-        for consumer in self._event_consumers:\n+        for consumer in self._event_consumers.values():\n             yield consumer.stopConsuming()\n-        self._event_consumers = []\n+        self._event_consumers = {}\n \n         for pending_call in list(self._pending_got_event_calls.values()):\n             yield pending_call\n", "issue": "There's a small window where events will be missed by reporters during reconfig\nThe code in `ReporterBase.reconfigService()` stops consuming events for all event keys and then starts consuming events for all event keys again. This is not necessary for any wanted event keys which are wanted in both old and new configuration. The code should stop consuming events for no longer wanted event keys, start consuming events for newly wanted event keys and leave the rest untouched.\n", "before_files": [{"content": "# This file is part of Buildbot.  Buildbot is free software: you can\n# redistribute it and/or modify it under the terms of the GNU General Public\n# License as published by the Free Software Foundation, version 2.\n#\n# This program is distributed in the hope that it will be useful, but WITHOUT\n# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more\n# details.\n#\n# You should have received a copy of the GNU General Public License along with\n# this program; if not, write to the Free Software Foundation, Inc., 51\n# Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n#\n# Copyright Buildbot Team Members\n\nimport abc\n\nfrom twisted.internet import defer\nfrom twisted.python import log\n\nfrom buildbot import config\nfrom buildbot.reporters import utils\nfrom buildbot.util import service\nfrom buildbot.util import tuplematch\n\nENCODING = 'utf-8'\n\n\nclass ReporterBase(service.BuildbotService):\n    name = None\n    __meta__ = abc.ABCMeta\n\n    compare_attrs = ['generators']\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self.generators = None\n        self._event_consumers = []\n        self._pending_got_event_calls = {}\n\n    def checkConfig(self, generators):\n        if not isinstance(generators, list):\n            config.error('{}: generators argument must be a list')\n\n        for g in generators:\n            g.check()\n\n        if self.name is None:\n            self.name = self.__class__.__name__\n            for g in generators:\n                self.name += \"_\" + g.generate_name()\n\n    @defer.inlineCallbacks\n    def reconfigService(self, generators):\n\n        for consumer in self._event_consumers:\n            yield consumer.stopConsuming()\n        self._event_consumers = []\n\n        self.generators = generators\n\n        wanted_event_keys = set()\n        for g in self.generators:\n            wanted_event_keys.update(g.wanted_event_keys)\n\n        for key in sorted(list(wanted_event_keys)):\n            consumer = yield self.master.mq.startConsuming(self._got_event, key)\n            self._event_consumers.append(consumer)\n\n    @defer.inlineCallbacks\n    def stopService(self):\n        for consumer in self._event_consumers:\n            yield consumer.stopConsuming()\n        self._event_consumers = []\n\n        for pending_call in list(self._pending_got_event_calls.values()):\n            yield pending_call\n        self._pending_got_event_calls = {}\n\n        yield super().stopService()\n\n    def _does_generator_want_key(self, generator, key):\n        for filter in generator.wanted_event_keys:\n            if tuplematch.matchTuple(key, filter):\n                return True\n        return False\n\n    def _get_chain_key_for_event(self, key, msg):\n        if key[0] in [\"builds\", \"buildrequests\"]:\n            return (\"buildrequestid\", msg[\"buildrequestid\"])\n        return None\n\n    @defer.inlineCallbacks\n    def _got_event(self, key, msg):\n        chain_key = self._get_chain_key_for_event(key, msg)\n        if chain_key is not None:\n            d = defer.Deferred()\n            pending_call = self._pending_got_event_calls.get(chain_key)\n            self._pending_got_event_calls[chain_key] = d\n            # Wait for previously pending call, if any, to ensure\n            # reports are sent out in the order events were queued.\n            if pending_call is not None:\n                yield pending_call\n\n        try:\n            reports = []\n            for g in self.generators:\n                if self._does_generator_want_key(g, key):\n                    report = yield g.generate(self.master, self, key, msg)\n                    if report is not None:\n                        reports.append(report)\n\n            if reports:\n                yield self.sendMessage(reports)\n        except Exception as e:\n            log.err(e, 'Got exception when handling reporter events')\n\n        if chain_key is not None:\n            if self._pending_got_event_calls.get(chain_key) == d:\n                del self._pending_got_event_calls[chain_key]\n            d.callback(None)  # This event is now fully handled\n\n    def getResponsibleUsersForBuild(self, master, buildid):\n        # Use library method but subclassers may want to override that\n        return utils.getResponsibleUsersForBuild(master, buildid)\n\n    @abc.abstractmethod\n    def sendMessage(self, reports):\n        pass\n", "path": "master/buildbot/reporters/base.py"}]}