STORY
I needed a method to check if the current hour is in the specified time range. So I wrote this:
@staticmethod
def isInTimeRange(time = datetime.datetime.now().hour):
#magic happen
- I tested locally.. and it works on my machine. Good.
- I push code to test environment and everything works.
- Great (however, it surprised me a bit as I expected to see some sort of teething troubles).
- On the day to deploy into production something went badly wrong.
I got very stressed because the outcome was very surprising to me and although I knew we could rollback changes I didn't know how to do it. We rolled back to the previous version and I started to investigate what went wrong.
At first, it looked I missed some configuration and I found that some information about configuration is incorrect too, so I fix them.
I decided to re-run tests.
- As always everything works locally.
- I run on the test environment and it still works.
- I run in pre-production environment ... it works at first but my application didn't work at night.
The application works locally, on the test env but it doesn't work at night. Hmm...
After an action-packed investigation, I narrowed down to my method. The first hint was ... maybe this doesn't work as I didn't specify a time zone, so I added. As well, I added more logs to see actual times.
@staticmethod
def isInTimeRange(time = datetime.datetime.now(pytz.timezone('Europe/London')).hour):
#magic happen
Results? Well.
- As always "It worked on my machine".
- It worked on the test environment.
- I see that 18.00, my method returns... 18.00.
- And then ... at night in pre-prod... At 4.00 (am) My method returns... 19.00.
Why 19.00? At first, it didn't click to me how I got this strange time, but when I checked when my app was deployed (It was deployed at 19.00), then I figure out what went wrong.
As it turns out, @staticmethod is run before anything else is initialised.
It means that the default value was evaluated "run" first and the result was "reused" after that. (I don't want to use word cached). It is something that hasn't been mention in the documentation (or more likely I didn't see it) so for the beginner like me in Python it was a big surprise.
SOLUTION
To solve problem In my case, we refactor the code to get time inside the method, which fixes a problem.
@staticmethod
def isInTimeRange():
time = datetime.datetime.now(pytz.timezone('Europe/London')).hour
#magic happen
What I learnt from my mistakes?
- Do one thing at a time. If your code suddenly requires changes to Jenkins and then version of Python, then test them first. This is something that bites me a few times in this case and I could avoid some problems.
- Being aware of rollback previous version on production is not enough. Make sure you know to how to rollback if something shit hit the fan. As I knew we had a solution for rollback but I didn't know how to execute it.
- It is better when the project is configurable than changes requires git commit. This will speed up testing.
- I was bitten by a false-positive result of one of my tests. Time-based tests require more design next time.
- Fix one problem at a time. It helps me fix things step by step.
Resources: