Commit d8144588ac46d1e18f8bb77f19ba6a856918556f

Authored by Miguel Barão
1 parent 6deb4a4f
Exists in master and in 1 other branch dev

- check_answer no longer blocks the tornado event_loop.

1 1
2 # BUGS 2 # BUGS
3 3
4 -- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado.  
5 -- change password modal nao aparece no ipad (safari e firefox)  
6 - on start topic, logs show questionhandler.get() twice. 4 - on start topic, logs show questionhandler.get() twice.
  5 +- change password modal nao aparece no ipad (safari e firefox)
7 - detect questions in questions.yaml without ref -> error ou generate default. 6 - detect questions in questions.yaml without ref -> error ou generate default.
8 -- Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos...  
9 -- generators not working: bcrypt (ver blog)  
10 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question. 7 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question.
11 8
12 # TODO 9 # TODO
13 10
14 -- session management. close after inactive time. 11 +- Criar outra estrutura organizada em capítulos (conjuntos de tópicos). Permitir capítulos de capítulos, etc. talvez usar grafos de grafos...
15 - each topic only loads a sample of K questions (max) in random order. 12 - each topic only loads a sample of K questions (max) in random order.
  13 +- session management. close after inactive time.
16 - radio e checkboxes, aceitar numeros como seleccao das opcoes. 14 - radio e checkboxes, aceitar numeros como seleccao das opcoes.
17 - reload das perguntas enquanto online. ver signal em http://stackabuse.com/python-async-await-tutorial/ 15 - reload das perguntas enquanto online. ver signal em http://stackabuse.com/python-async-await-tutorial/
18 - pertuntas tipo tristate: (sim, não, não sei 16 - pertuntas tipo tristate: (sim, não, não sei
@@ -29,6 +27,7 @@ @@ -29,6 +27,7 @@
29 27
30 # FIXED 28 # FIXED
31 29
  30 +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado.
32 - servir imagens/ficheiros. 31 - servir imagens/ficheiros.
33 - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa). 32 - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa).
34 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória. 33 - checkbox: cada opção pode ser uma dupla (certo, errado) sendo escolhida uma aleatória.
demo/solar_system/questions.yaml
@@ -6,7 +6,7 @@ @@ -6,7 +6,7 @@
6 type: radio 6 type: radio
7 title: Sistema solar 7 title: Sistema solar
8 text: | 8 text: |
9 - ![planetas](planetsa.png " Planetas do Sistema Solar") 9 + ![planetas](planets.png " Planetas do Sistema Solar")
10 10
11 Qual é o maior planeta do Sistema Solar? 11 Qual é o maior planeta do Sistema Solar?
12 options: 12 options:
@@ -152,7 +152,7 @@ class LearnApp(object): @@ -152,7 +152,7 @@ class LearnApp(object):
152 try: 152 try:
153 ok = self.online[uid]['state'].init_topic(topic) 153 ok = self.online[uid]['state'].init_topic(topic)
154 except KeyError as e: 154 except KeyError as e:
155 - logger.warning(f'User "{uid}" denied nonexistent "{topic}"') 155 + logger.warning(f'User "{uid}" tried to open nonexistent topic: "{topic}"')
156 raise e 156 raise e
157 else: 157 else:
158 if ok: 158 if ok:
@@ -7,6 +7,7 @@ import base64 @@ -7,6 +7,7 @@ import base64
7 import uuid 7 import uuid
8 import logging.config 8 import logging.config
9 import argparse 9 import argparse
  10 +from concurrent.futures import ThreadPoolExecutor
10 11
11 # user installed libraries 12 # user installed libraries
12 import tornado.ioloop 13 import tornado.ioloop
@@ -14,6 +15,9 @@ import tornado.web @@ -14,6 +15,9 @@ import tornado.web
14 import tornado.httpserver 15 import tornado.httpserver
15 from tornado import template #, gen 16 from tornado import template #, gen
16 17
  18 +from tornado.concurrent import run_on_executor
  19 +from tornado.platform.asyncio import to_tornado_future
  20 +
17 # this project 21 # this project
18 from learnapp import LearnApp 22 from learnapp import LearnApp
19 from tools import load_yaml, md_to_html 23 from tools import load_yaml, md_to_html
@@ -142,15 +146,18 @@ class TopicHandler(BaseHandler): @@ -142,15 +146,18 @@ class TopicHandler(BaseHandler):
142 else: 146 else:
143 self.redirect('/') 147 self.redirect('/')
144 148
  149 +
145 # ---------------------------------------------------------------------------- 150 # ----------------------------------------------------------------------------
  151 +# Serves files from the /public subdir of the topics.
146 # Based on https://bhch.github.io/posts/2017/12/serving-large-files-with-tornado-safely-without-blocking/ 152 # Based on https://bhch.github.io/posts/2017/12/serving-large-files-with-tornado-safely-without-blocking/
  153 +# ----------------------------------------------------------------------------
147 class FileHandler(BaseHandler): 154 class FileHandler(BaseHandler):
148 @tornado.web.authenticated 155 @tornado.web.authenticated
149 async def get(self, filename): 156 async def get(self, filename):
150 uid = self.current_user 157 uid = self.current_user
151 public_dir = self.learn.get_current_public_dir(uid) 158 public_dir = self.learn.get_current_public_dir(uid)
152 filepath = path.expanduser(path.join(public_dir, filename)) 159 filepath = path.expanduser(path.join(public_dir, filename))
153 - chunk_size = 1024 * 1024 # serve 1MiB multiple times 160 + chunk_size = 1024 * 1024 # serve up to 1MiB multiple times
154 try: 161 try:
155 f = open(filepath, 'rb') 162 f = open(filepath, 'rb')
156 except FileNotFoundError: 163 except FileNotFoundError:
@@ -159,9 +166,8 @@ class FileHandler(BaseHandler): @@ -159,9 +166,8 @@ class FileHandler(BaseHandler):
159 logging.error(f'No permission: {filepath}') 166 logging.error(f'No permission: {filepath}')
160 else: 167 else:
161 with f: 168 with f:
162 - while True:  
163 - chunk = f.read(chunk_size)  
164 - if not chunk: break 169 + chunk = f.read(chunk_size)
  170 + while chunk:
165 try: 171 try:
166 self.write(chunk) # write the cunk to response 172 self.write(chunk) # write the cunk to response
167 await self.flush() # flush the current chunk to socket 173 await self.flush() # flush the current chunk to socket
@@ -172,12 +178,15 @@ class FileHandler(BaseHandler): @@ -172,12 +178,15 @@ class FileHandler(BaseHandler):
172 del chunk 178 del chunk
173 await gen.sleep(0.000000001) # 1 nanosecond (hack) 179 await gen.sleep(0.000000001) # 1 nanosecond (hack)
174 # in tornnado 5.0 use `await asyncio.sleep(0)` instead 180 # in tornnado 5.0 use `await asyncio.sleep(0)` instead
  181 + chunk = f.read(chunk_size)
175 182
176 183
177 # ---------------------------------------------------------------------------- 184 # ----------------------------------------------------------------------------
178 # respond to AJAX to get a JSON question 185 # respond to AJAX to get a JSON question
179 # ---------------------------------------------------------------------------- 186 # ----------------------------------------------------------------------------
180 class QuestionHandler(BaseHandler): 187 class QuestionHandler(BaseHandler):
  188 + executor = ThreadPoolExecutor(max_workers=2)
  189 +
181 templates = { 190 templates = {
182 'checkbox': 'question-checkbox.html', 191 'checkbox': 'question-checkbox.html',
183 'radio': 'question-radio.html', 192 'radio': 'question-radio.html',
@@ -194,6 +203,13 @@ class QuestionHandler(BaseHandler): @@ -194,6 +203,13 @@ class QuestionHandler(BaseHandler):
194 # 'alert': '', FIXME 203 # 'alert': '', FIXME
195 } 204 }
196 205
  206 + # Blocking function to be run on the executor
  207 + @run_on_executor()
  208 + def check_answer(self, user, answer):
  209 + return self.learn.check_answer(user, answer)
  210 +
  211 +
  212 + # --- get question to render
197 @tornado.web.authenticated 213 @tornado.web.authenticated
198 def get(self): 214 def get(self):
199 logging.debug('QuestionHandler.get()') 215 logging.debug('QuestionHandler.get()')
@@ -211,9 +227,9 @@ class QuestionHandler(BaseHandler): @@ -211,9 +227,9 @@ class QuestionHandler(BaseHandler):
211 } 227 }
212 }) 228 })
213 229
214 - # handles answer posted 230 + # --- post answer, returns what to do next: shake, new_question, finished
215 @tornado.web.authenticated 231 @tornado.web.authenticated
216 - def post(self): 232 + async def post(self):
217 logging.debug('QuestionHandler.post()') 233 logging.debug('QuestionHandler.post()')
218 user = self.current_user 234 user = self.current_user
219 235
@@ -229,7 +245,8 @@ class QuestionHandler(BaseHandler): @@ -229,7 +245,8 @@ class QuestionHandler(BaseHandler):
229 elif qtype != 'checkbox': # radio, text, textarea, ... 245 elif qtype != 'checkbox': # radio, text, textarea, ...
230 answer = answer[0] 246 answer = answer[0]
231 247
232 - grade = self.learn.check_answer(user, answer) 248 + # check answer in another thread (nonblocking)
  249 + grade = await to_tornado_future(self.check_answer(user, answer))
233 question = self.learn.get_student_question(user) 250 question = self.learn.get_student_question(user)
234 251
235 if grade <= 0.999: # wrong answer 252 if grade <= 0.999: # wrong answer
templates/topic.html
@@ -91,9 +91,7 @@ @@ -91,9 +91,7 @@
91 <footer class="footer"> 91 <footer class="footer">
92 <div class="container"> 92 <div class="container">
93 <button class="btn btn-primary btn-lg btn-block my-3" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button> 93 <button class="btn btn-primary btn-lg btn-block my-3" id="submit" data-toggle="tooltip" data-placement="right" title="Shift-Enter">Continuar</button>
94 -<!--  
95 - <span class="text-muted"></span>  
96 - --> </div> 94 + </div>
97 </footer> 95 </footer>
98 96
99 </body> 97 </body>