找出缺陷!使用任务队列可靠地执行长任务
我正在谷歌应用程序引擎上制作成绩册。我跟踪每个评分期每个学生的成绩。评分期可以重叠。由于我可能一次显示数百个这样的成绩,因此我在服务器上预先计算了成绩。因此,对于任何一名学生,我可能有许多计算出的成绩 - 每个评分期都有一个成绩。
现在,老师输入测验的新分数。该分数可能会影响许多计算的成绩,因为它可能属于许多评分期。我需要重新计算所有受影响的成绩。这可能需要很长时间,因为对于每个评分周期,我都需要获取所有相关分数并对这些分数执行复杂的例程。我认为 30 秒是不够的 - 特别是如果数据存储今天感觉很慢的话。此外,失败不是一种选择。某些成绩更新而另一些成绩却悄然过时,这是不可接受的。
所以我心里想,这是一个学习任务队列的好时机!
我不是数据库结构或其他方面的专家,但这里是我想做的事情的概述:
public ReturnCode addNewScore(Float score, Date date, Long studentId)
{
List<CalculatedGrade> existingGrades = getAllRelevantGradesForStudent(studentId, date);
for (CalculatedGrade grade : existingGrades)
{
grade.markDirty(); //leaves a record that this grade is no longer up to date
}
persistenceManager.makePersistentAll(existingGrades);
//DANGER ZONE?
persistenceManager.makePersistent(new IndividualScore(score, date, studentId));
tellTheTaskQueueToStartCalculating();
return OMG_IT_WORKED;
}
这似乎是一种将所有相关等级标记为脏的快速方法。如果中途失败,则会返回失败,客户端会知道要重试。如果客户端稍后尝试获取脏成绩,我们可以在那里返回错误。
然后,任务队列代码将如下所示:
public void calculateThemGrades()
{
List<CalculatedGrade> dirtyGrades = getAllDirtyGrades();
try
{
for (CalculatedGrade grade : dirtyGrades)
{
List<Score> relevantScores = getAllRelevantScores();
Float cleanGrade = calculateGrade(relevantScores);
grade.setGrade(cleanGrade);
grade.markClean();
persistenceManager.flush();
}
}
catch(Throwable anything)
{
//if there was any problem, like we ran out of time or the datastore is down or whatever, just try again
tellTheTaskQueueToStartCalculating()
}
}
这是我的问题:这是否保证在添加新分数后永远不会有一个计算成绩被标记为干净?
需要关注的特定领域:
- 在危险区域周围的第一个片段中,
existingGrades
是否始终会保留在新的IndividualScore
之前? - 是否有可能另一个线程会在危险区域启动任务队列代码,以便在真正输入新的
IndividualScore
之前,那些现有的成绩可能会再次被标记为干净?如果是这样,我如何确保不会发生这种情况(所有年级的交易都已结束)? - 即使 pm 未关闭,persistenceManager.flush() 是否足以保存部分完成的计算?
这一定是一个常见的问题。我很感激任何教程的链接,特别是那些关于 appengine 的链接。感谢您阅读了这么多!
I'm making a gradebook on google app engine. I keep track of each student's grade per grading period. The grading periods can overlap. Since I may display hundreds of these grades at a time, I precalculate the grades on the server. So, for any one student, I may have many calculated grades - one for each grading period.
Now, the teacher enters a new score from a quiz. That score may affect many of the calculated grades, because it may fall into many grading periods. I need to recalculate all of the affected grades. This could take a long time, since for each grading period I need to fetch all relevant scores and do a complex routine over those scores. I think 30 seconds isn't enough - especially if the datastore is feeling slow today. Furthermore, failure is not an option. It is unacceptable for some grades to update and others to fall silently out of date.
So I think to myself, what a wonderful time to learn about the task queue!
I'm not an expert in DB structure or anything, but here's an outline of what I want to do:
public ReturnCode addNewScore(Float score, Date date, Long studentId)
{
List<CalculatedGrade> existingGrades = getAllRelevantGradesForStudent(studentId, date);
for (CalculatedGrade grade : existingGrades)
{
grade.markDirty(); //leaves a record that this grade is no longer up to date
}
persistenceManager.makePersistentAll(existingGrades);
//DANGER ZONE?
persistenceManager.makePersistent(new IndividualScore(score, date, studentId));
tellTheTaskQueueToStartCalculating();
return OMG_IT_WORKED;
}
This seems like a fast way to mark all of the relevant grades dirty. If it fails half-way through, then failure is returned and the client will know to try again. If a client later tries to fetch a dirty grade, we can return an error there.
Then, the task queue code would look something like this:
public void calculateThemGrades()
{
List<CalculatedGrade> dirtyGrades = getAllDirtyGrades();
try
{
for (CalculatedGrade grade : dirtyGrades)
{
List<Score> relevantScores = getAllRelevantScores();
Float cleanGrade = calculateGrade(relevantScores);
grade.setGrade(cleanGrade);
grade.markClean();
persistenceManager.flush();
}
}
catch(Throwable anything)
{
//if there was any problem, like we ran out of time or the datastore is down or whatever, just try again
tellTheTaskQueueToStartCalculating()
}
}
Here's my question: does this guarantee that there will never be a calculated grade that is marked clean after a new score has been added?
Specific areas of concern:
- will the
existingGrades
always be persisted before the newIndividualScore
in the first snippet, around the danger zone? - Is it possible that another thread will start the task queue code in the danger zone so that those existingGrades might be marked clean again before the new
IndividualScore
is really entered? If so, how can I make sure that won't happen (transactions across all of the grades are out)? - Is
persistenceManager.flush()
enough to save partially-done calculations, even though the pm is not closed?
This must be a common sort of problem. I'd appreciate any links to tutorials, especially those for appengine. Thanks for reading so much!
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
如果您担心竞争条件,请不要使用布尔脏标志 - 相反,请使用一对时间戳。当您想要将记录标记为脏时,请更新“脏”时间戳。
当您开始计算成绩时,请记下“脏”时间戳。
完成成绩计算后,将“干净”时间戳更新为等于您开始时读取的“脏”时间戳的值,这表示您已将该成绩与截至该时间戳的新数据同步。
任何“脏”时间戳大于其“干净”时间戳的记录都是脏的。任何两场比赛都是干净的记录。简单有效。如果另一个请求添加了会影响给定成绩的新数据,而您的任务队列任务已经在计算成绩的中间,则“脏”时间戳将与更新的“干净”时间戳不匹配,因此任务队列将考虑该记录还是脏了,再处理一下。
If you're worried about race conditions, don't use a boolean dirty flag - instead, use a pair of timestamps. When you want to mark a record dirty, update the 'dirty' timestamp.
When you start calculating the grade, make a note of what the 'dirty' timestamp was.
When you finish calculating the grade, update the 'clean' timestamp to be equal to the value of the 'dirty' timestamp you read when you began, signifying that you've synchronized that grade with the new data as of that timestamp.
Any record with a 'dirty' timestamp greater than its 'clean' timestamp is dirty. Any record where the two match is clean. Simple and effective. If another request adds new data that would affect a given grade while your taskqueue task is already in the middle of calculating the grade, the 'dirty' timestamp won't match the updated 'clean' timestamp, and thus the taskqueue will consider the record still dirty and process it again.