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.

BUGS.md
1 1  
2 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 4 - on start topic, logs show questionhandler.get() twice.
  5 +- change password modal nao aparece no ipad (safari e firefox)
7 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 7 - tabelas nas perguntas radio/checkbox não ocupam todo o espaço como em question.
11 8  
12 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 12 - each topic only loads a sample of K questions (max) in random order.
  13 +- session management. close after inactive time.
16 14 - radio e checkboxes, aceitar numeros como seleccao das opcoes.
17 15 - reload das perguntas enquanto online. ver signal em http://stackabuse.com/python-async-await-tutorial/
18 16 - pertuntas tipo tristate: (sim, não, não sei
... ... @@ -29,6 +27,7 @@
29 27  
30 28 # FIXED
31 29  
  30 +- generators e correct scripts que durem muito tempo bloqueiam o eventloop do tornado.
32 31 - servir imagens/ficheiros.
33 32 - radio: suporte para multiplas opcoes correctas e erradas, escolhendo-se uma selecção aleatoria destas (so com 1 certa).
34 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 6 type: radio
7 7 title: Sistema solar
8 8 text: |
9   - ![planetas](planetsa.png " Planetas do Sistema Solar")
  9 + ![planetas](planets.png " Planetas do Sistema Solar")
10 10  
11 11 Qual é o maior planeta do Sistema Solar?
12 12 options:
... ...
learnapp.py
... ... @@ -152,7 +152,7 @@ class LearnApp(object):
152 152 try:
153 153 ok = self.online[uid]['state'].init_topic(topic)
154 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 156 raise e
157 157 else:
158 158 if ok:
... ...
serve.py
... ... @@ -7,6 +7,7 @@ import base64
7 7 import uuid
8 8 import logging.config
9 9 import argparse
  10 +from concurrent.futures import ThreadPoolExecutor
10 11  
11 12 # user installed libraries
12 13 import tornado.ioloop
... ... @@ -14,6 +15,9 @@ import tornado.web
14 15 import tornado.httpserver
15 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 21 # this project
18 22 from learnapp import LearnApp
19 23 from tools import load_yaml, md_to_html
... ... @@ -142,15 +146,18 @@ class TopicHandler(BaseHandler):
142 146 else:
143 147 self.redirect('/')
144 148  
  149 +
145 150 # ----------------------------------------------------------------------------
  151 +# Serves files from the /public subdir of the topics.
146 152 # Based on https://bhch.github.io/posts/2017/12/serving-large-files-with-tornado-safely-without-blocking/
  153 +# ----------------------------------------------------------------------------
147 154 class FileHandler(BaseHandler):
148 155 @tornado.web.authenticated
149 156 async def get(self, filename):
150 157 uid = self.current_user
151 158 public_dir = self.learn.get_current_public_dir(uid)
152 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 161 try:
155 162 f = open(filepath, 'rb')
156 163 except FileNotFoundError:
... ... @@ -159,9 +166,8 @@ class FileHandler(BaseHandler):
159 166 logging.error(f'No permission: {filepath}')
160 167 else:
161 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 171 try:
166 172 self.write(chunk) # write the cunk to response
167 173 await self.flush() # flush the current chunk to socket
... ... @@ -172,12 +178,15 @@ class FileHandler(BaseHandler):
172 178 del chunk
173 179 await gen.sleep(0.000000001) # 1 nanosecond (hack)
174 180 # in tornnado 5.0 use `await asyncio.sleep(0)` instead
  181 + chunk = f.read(chunk_size)
175 182  
176 183  
177 184 # ----------------------------------------------------------------------------
178 185 # respond to AJAX to get a JSON question
179 186 # ----------------------------------------------------------------------------
180 187 class QuestionHandler(BaseHandler):
  188 + executor = ThreadPoolExecutor(max_workers=2)
  189 +
181 190 templates = {
182 191 'checkbox': 'question-checkbox.html',
183 192 'radio': 'question-radio.html',
... ... @@ -194,6 +203,13 @@ class QuestionHandler(BaseHandler):
194 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 213 @tornado.web.authenticated
198 214 def get(self):
199 215 logging.debug('QuestionHandler.get()')
... ... @@ -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 231 @tornado.web.authenticated
216   - def post(self):
  232 + async def post(self):
217 233 logging.debug('QuestionHandler.post()')
218 234 user = self.current_user
219 235  
... ... @@ -229,7 +245,8 @@ class QuestionHandler(BaseHandler):
229 245 elif qtype != 'checkbox': # radio, text, textarea, ...
230 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 250 question = self.learn.get_student_question(user)
234 251  
235 252 if grade <= 0.999: # wrong answer
... ...
templates/topic.html
... ... @@ -91,9 +91,7 @@
91 91 <footer class="footer">
92 92 <div class="container">
93 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 95 </footer>
98 96  
99 97 </body>
... ...