|
3 | 3 | # Copyright (C) 2019 - 2020 Tuono, Inc.
|
4 | 4 | # All Rights Reserved
|
5 | 5 | #
|
| 6 | +import difflib |
| 7 | +import io |
6 | 8 | import logging
|
7 | 9 | import math
|
8 |
| -import os |
9 | 10 | import pickle # nosec
|
| 11 | +import pickletools # nosec |
10 | 12 | import shelve
|
11 | 13 |
|
12 | 14 | from contextlib import AbstractContextManager
|
@@ -334,6 +336,7 @@ def playback(self, context: CallContext, channel: str = "default") -> Any:
|
334 | 336 | uniq = self._advance(context, channel)
|
335 | 337 | recorded = self._tape.get(uniq, RecordedCallNotFoundError(context))
|
336 | 338 | if isinstance(recorded, RecordedCallNotFoundError):
|
| 339 | + self._forensics(context, channel) |
337 | 340 | raise recorded
|
338 | 341 |
|
339 | 342 | payload = recorded
|
@@ -398,27 +401,78 @@ def _advance(self, context: CallContext, channel: str) -> str:
|
398 | 401 | our_meta[self.LABEL_HASH] = result
|
399 | 402 | return result
|
400 | 403 |
|
| 404 | + def _forensics(self, context: CallContext, channel: str) -> None: |
| 405 | + """ |
| 406 | + Perform forensic analysis of RecordedCallNotFoundError and log: |
| 407 | +
|
| 408 | + - The recorded pickled context (stored in _call_<channel>_<ordinal>) |
| 409 | + - The playback pickled context |
| 410 | + - The difference |
| 411 | + """ |
| 412 | + ordinal = self._call_ordinals[channel] |
| 413 | + |
| 414 | + recorded_raw = self._tape.get(f"_call_{channel}_{ordinal}") |
| 415 | + playback_call = self._reduce_call(context) |
| 416 | + try: |
| 417 | + playback_raw = self._redact(context, return_bytes=True) |
| 418 | + finally: |
| 419 | + context.call = playback_call |
| 420 | + |
| 421 | + assert recorded_raw != playback_raw, "why did we get RecordedCallNotFoundError?" |
| 422 | + |
| 423 | + if recorded_raw is not None: |
| 424 | + recorded_io = io.StringIO() |
| 425 | + pickletools.dis(recorded_raw, out=recorded_io) |
| 426 | + recorded_transcript = recorded_io.getvalue() |
| 427 | + self._log( |
| 428 | + logging.DEBUG, |
| 429 | + "mismatch", |
| 430 | + "recorded", |
| 431 | + f"RECORDED CALL IN CHANNEL {channel} ORDINAL {ordinal}:\n\n{recorded_transcript}", |
| 432 | + ) |
| 433 | + else: |
| 434 | + self._log( |
| 435 | + logging.DEBUG, |
| 436 | + "mismatch", |
| 437 | + "recorded", |
| 438 | + f"NO RECORDED CALL IN CHANNEL {channel} ORDINAL {ordinal}", |
| 439 | + ) |
| 440 | + |
| 441 | + playback_io = io.StringIO() |
| 442 | + pickletools.dis(playback_raw, out=playback_io) |
| 443 | + playback_transcript = playback_io.getvalue() |
| 444 | + |
| 445 | + self._log( |
| 446 | + logging.DEBUG, |
| 447 | + "mismatch", |
| 448 | + "playback", |
| 449 | + f"PLAYBACK CALL IN CHANNEL {channel} ORDINAL {ordinal}:\n\n{playback_transcript}", |
| 450 | + ) |
| 451 | + |
| 452 | + if recorded_raw and playback_raw: |
| 453 | + differences = list( |
| 454 | + difflib.context_diff( |
| 455 | + recorded_transcript.splitlines(), playback_transcript.splitlines() |
| 456 | + ) |
| 457 | + ) |
| 458 | + self._log(logging.DEBUG, "mismatch", "difference", "\n".join(differences)) |
| 459 | + |
401 | 460 | def _hickle(self, context: CallContext) -> str:
|
402 | 461 | """
|
403 |
| - Hash a context using a redacted pickle. |
| 462 | + Hash a context using a redacted pickle. In addition we stuff |
| 463 | + the original redacted call into the database so we can compare |
| 464 | + that call's raw content against a playback call to see why they |
| 465 | + are different. |
404 | 466 |
|
405 | 467 | Raises:
|
406 | 468 | PicklingError if something in the context cannot be pickled.
|
407 | 469 | """
|
408 | 470 | raw = self._redact(context, return_bytes=True)
|
409 |
| - # if TAPEDECKDEBUG is in the environment we dump out the raw pickles so |
410 |
| - # we can use "python3 -m pickletools <file>" to dump out the actual |
411 |
| - # raw pickle content and determine why there was a mismatch; to be used |
412 |
| - # when playback raises RecordedCallNotFoundError |
413 |
| - if "TAPEDECKDEBUG" in os.environ and context: |
414 |
| - calldir = Path(str(self.deck) + "-calls") |
415 |
| - calldir.mkdir(exist_ok=True) |
| 471 | + if self.mode == Mode.Recording: |
416 | 472 | our_meta = context.meta[self.LABEL_TAPE]
|
417 | 473 | channel = our_meta[self.LABEL_CHANNEL]
|
418 | 474 | ordinal = our_meta[self.LABEL_ORDINAL]
|
419 |
| - fname = f"{('record' if self.mode == Mode.Recording else 'playback')}-{channel}-{ordinal}.pickle" |
420 |
| - with (calldir / fname).open("wb") as fp: |
421 |
| - fp.write(raw) |
| 475 | + self._tape[f"_call_{channel}_{ordinal}"] = raw |
422 | 476 | uniq = sha256(raw)
|
423 | 477 | result = uniq.hexdigest()
|
424 | 478 | return result
|
|
0 commit comments