Repository: Rapptz/discord.py

Issue:
discord.ext.tasks adds time to the next iteration on retry
### Summary

<!-- A summary of your bug report -->

When the Task hits an exception that it can reconnect on, it will add the loop time to the next iteration counter. This is obviously not right, because it should be the loop time from the latest retry.

### Reproduction Steps

<!-- What you did to make it happen. Ideally there should be a short code snippet in this section to help reproduce the bug. -->

```python
import asyncio
import datetime

import discord.ext.tasks


@discord.ext.tasks.loop(hours=1)
async def do_something():
    # await asyncio.sleep(1)
    print(str(do_something.next_iteration))
    if (do_something.next_iteration.replace(tzinfo=None) - datetime.datetime.utcnow()) > datetime.timedelta(hours=4):
        do_something.stop()
        return
    raise ValueError("Force Loop Reconnect")


do_something.add_exception_type(ValueError)


async def start():
    await do_something.start()


loop = asyncio.get_event_loop()
loop.run_until_complete(start())
```

### Expected Results

<!-- What you expected to happen -->

The output to be: 

```
2020-08-28 04:11:12.031077+00:00
2020-08-28 04:11:13.031077+00:00
2020-08-28 04:11:14.031077+00:00
2020-08-28 04:11:15.031077+00:00
2020-08-28 04:11:16.031077+00:00

Process finished with exit code 0
```

### Actual Results

<!-- What actually happened. If there is a traceback, please show the entire thing. -->

```
2020-08-28 04:11:12.031077+00:00
2020-08-28 05:11:12.031077+00:00
2020-08-28 06:11:12.031077+00:00
2020-08-28 07:11:12.031077+00:00
2020-08-28 08:11:12.031077+00:00

Process finished with exit code 0
```


### Checklist

<!-- Put an x inside [ ] to check it, like so: [x] -->

- [x] I have searched the open issues for duplicates.
- [x] I have shown the entire traceback, if possible.
- [x] I have removed my token from display, if visible.

### System Information

<!-- Run `python -m discord -v` and paste this information below. -->
<!-- This command is available in v1.1.0 or higher. If you are unable to run this command, paste basic info (ie. Python version, library version, and your operating system -->

```
- Python v3.8.5-final
- discord.py v1.4.1-final
- aiohttp v3.6.2
- system info: Darwin 19.6.0 Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64
```



Discussion:

Resolving patch:
diff --git a/discord/ext/tasks/__init__.py b/discord/ext/tasks/__init__.py
--- a/discord/ext/tasks/__init__.py
+++ b/discord/ext/tasks/__init__.py
@@ -68,6 +68,7 @@ def __init__(self, coro, seconds, hours, minutes, count, reconnect, loop):
             raise ValueError('count must be greater than 0 or None.')
 
         self.change_interval(seconds=seconds, minutes=minutes, hours=hours)
+        self._last_iteration_failed = False
         self._last_iteration = None
         self._next_iteration = None
 
@@ -88,18 +89,22 @@ async def _loop(self, *args, **kwargs):
         backoff = ExponentialBackoff()
         await self._call_loop_function('before_loop')
         sleep_until = discord.utils.sleep_until
+        self._last_iteration_failed = False
         self._next_iteration = datetime.datetime.now(datetime.timezone.utc)
         try:
             await asyncio.sleep(0) # allows canceling in before_loop
             while True:
-                self._last_iteration = self._next_iteration
-                self._next_iteration = self._get_next_sleep_time()
+                if not self._last_iteration_failed:
+                    self._last_iteration = self._next_iteration
+                    self._next_iteration = self._get_next_sleep_time()
                 try:
                     await self.coro(*args, **kwargs)
+                    self._last_iteration_failed = False
                     now = datetime.datetime.now(datetime.timezone.utc)
                     if now > self._next_iteration:
                         self._next_iteration = now
                 except self._valid_exception as exc:
+                    self._last_iteration_failed = True
                     if not self.reconnect:
                         raise
                     await asyncio.sleep(backoff.delay())


Test patch: