Repository: faust-streaming/faust (Python)

Issue:
HoppingWindow
with the hopping window example, when I setting window=10, step=5, the result shows the window size can never reach 10. 
I knew that the hopping function is always creating a new window when a step is equal to 5. 
But how can I correctly have a 10 size window?

The code below is my attempt to implement a hopping window. 
Would anyone kindly tell me is that the correct way to go?

```
import sys
from datetime import datetime, timedelta
from time import time
import faust


class RawModel(faust.Record):
    date: datetime
    value: float


class WinModel(faust.Record):
    win: list


TOPIC = 'raw-event'
SINK = 'agg-event'
TABLE = 'tumbling_table'
KAFKA = 'kafka://localhost:9092'
CLEANUP_INTERVAL = 10
WINDOW = 6
STEP = 3
WINDOW_EXPIRES = 60
PARTITIONS = 1

app = faust.App('windowed-agg', broker=KAFKA, topic_partitions=1)

app.conf.table_cleanup_interval = CLEANUP_INTERVAL
source = app.topic(TOPIC, value_type=RawModel)
sink = app.topic(SINK, value_type=WinModel)

window_table = (
    app.Table(
        TABLE,
        default=list,
        partitions=PARTITIONS,
    )
    .tumbling(timedelta(seconds=WINDOW), expires=timedelta(seconds=WINDOW_EXPIRES))
    .relative_to_field(RawModel.date)
)


@app.agent(source)
async def print_windowed_events(stream):
    async for event in stream:

        value_list = window_table['events'].value()

        if len(value_list) > 0:
            event.value = value_list[-1].value + 1
        # print("Receive message : " + str(event))

        value_list.append(event)
        window_table['events'] = value_list

        a_list = (window_table["events"].delta(WINDOW) + window_table["events"].current())[-WINDOW:]

        await sink.send(value=WinModel(value=a_list))


@app.agent(sink)
async def print_trigger_events(stream):
    async for events in stream.take(100, within=timedelta(seconds=STEP)):
        print("windows : " + str(events[-1]))


@app.timer(1)
async def produce():
    value = 1
    await source.send(value=RawModel(value=value, date=int(time())))
    # print(f'Produce Message :: send messge {value}')


if __name__ == '__main__':
    if len(sys.argv) < 2:
        sys.argv.extend(['worker', '-l', 'info'])
    app.main()
```


Discussion: I try to make hopping windows works, check out robinhood/faust#514 and my fork https://github.com/doncat99/faust for more detail.



Fix (patch):
diff --git a/faust/tables/base.py b/faust/tables/base.py
index e3574ee21..ca26c0f11 100644
--- a/faust/tables/base.py
+++ b/faust/tables/base.py
@@ -382,13 +382,40 @@ async def _del_old_keys(self) -> None:
         for partition, timestamps in self._partition_timestamps.items():
             while timestamps and window.stale(timestamps[0], time.time()):
                 timestamp = heappop(timestamps)
+                triggered_windows = [
+                    self._partition_timestamp_keys.get(
+                        (partition, window_range)
+                    )  # noqa
+                    for window_range in self._window_ranges(timestamp)
+                ]
                 keys_to_remove = self._partition_timestamp_keys.pop(
                     (partition, timestamp), None
                 )
+                window_data = {}
                 if keys_to_remove:
-                    for key in keys_to_remove:
-                        value = self.data.pop(key, None)
-                        await self.on_window_close(key, value)
+                    for windows in triggered_windows:
+                        if windows:
+                            for processed_window in windows:
+                                # we use set to avoid duplicate element in window's data
+                                # window[0] is the window's key
+                                # it is not related to window's timestamp
+                                # windows are in format:
+                                # (key, (window_start, window_end))
+                                window_data.setdefault(processed_window[0], []).extend(
+                                    self.data.get(processed_window, [])
+                                )
+
+                    for key_to_remove in keys_to_remove:
+                        value = self.data.pop(key_to_remove, None)
+                        if key_to_remove[1][0] > self.last_closed_window:
+                            await self.on_window_close(
+                                key_to_remove,
+                                (
+                                    window_data[key_to_remove[0]]
+                                    if key_to_remove[0] in window_data
+                                    else value
+                                ),
+                            )
                     self.last_closed_window = max(
                         self.last_closed_window,
                         max(key[1][0] for key in keys_to_remove),