Yo Swiz, some users complained their exam ends immediately.
Hm… that's weird ?
So began my two day hunt for a bug.
To become a Yup tutor, you have to pass a subject exam: 25 questions about math, chemistry, or physics in 45 minutes. It's hard as hell; I tried.
There's a timer in the corner ticking down. 45 minutes, and then you're done. A dialog shows up telling you time’s up. Easy peasy to build, right? Get a start time, add 45 minutes, count down. When timer hits zero, stop. 90% of the time, it works every time.
And then there’re those 7 people. For them, the timer hits zero 1 second after it starts. 45 minutes. Tick. Zero.
o.O
I looked through the code. No race conditions. You click Start, the server creates an exam, makes a started_at
timestamp, adds 45 minutes, and tells the frontend when your exam should end. On every Clock tick, the counter updates ,and a MobX observer checks if you're out of time.
Very straightforward.
We're using React and MobX, so the frontend behaves like a state machine. There's no way we're rendering stale views, no way for poorly garbage collected views to cause problems, and the more I played with the frontend state machine, the sturdier it looked.
Here's an excerpt:
class SubjectExam {
constructor(user, subject) {
when(
() => this.isOutOfTime,
() => {
if (!this.finished) {
this.finish();
}
}
);
}
@computed get isOutOfTime() {
return !_.isNull(this.timeLeft) && this.timeLeft <= 0;
}
@computed get timeLeft() {
if (this.should_end_by) {
return moment.duration(moment.unix(this.should_end_by).diff(Clock.time));
} else {
return null;
}
}
}
MobX's when
creates an autorun that checks the condition function, () => this.isOutOfTime
, every time its value changes. When it evaluates to true
, it runs the given function. In this case, that’s an action that finishes the subject exam.
The reason this works is hidden in @computed timeLeft()
. It depends on Clock.time
, which is a global MobX store that updates every second. It’s based on my Modeling time in React & MobX approach.
Diffing the server-provided should_end_by
timestamp and Clock.time
tells us how much time is left. It works great and produces a countdown timer when you add some rendering.
The nice feature of this approach is that the timer is stable. You can refresh and navigate the page as much as you want. The server calculates the time, so it doesn't care.
But… those 7 people… why was the time up after 1 second? Can you guess?
I found no pattern in who the applicants were, when they took the exam, what timezone they were in, or which browser they used. It's not a bug in MomentJS. None of the cases happened around a DST, day, month, or year boundary.
It should work.
?
The only remaining explanation is that their system time was waaaaaayyy in the future. More than 45 minutes. I don't know why their computers thought they were in the future, but it's the only explanation that works.
How do you fix it?
You don't rely on user time. Instead of calculating a should_end_by
timestamp on the server, calculate the delta. Look only at the delta when doing a countdown.
Like this:
The exam starts, the server looks at current time and returns 45min
. User reloads, server looks at current time, calculates delta from start time, returns new time remaining.
On the frontend, you create a local shouldEndAt
timestamp when you load the exam – time + delta_from_server
– then count down from there.
Lesson learned. Time is so hard that you can't even rely on people's clocks being accurate to within 1 hour. Add it to the Falsehoods Programmers Believe About Time pile.
?
Continue reading about Yet another hard lesson about time
Semantically similar articles hand-picked by GPT-4
- Modeling time in React
- One awkward thing about MobX: Complex models
- A day is not 60*60*24 seconds long
- Words that scare developers
- More messing with time: Deduping messages between iOS and JavaScript
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️