TT Lab
Blog

한국어 English 中文

Get started
Blog

Blog

The Day a Single Number Halted Deployment: 729 in 268,729 and Regex Word Boundaries

Status: Resolved (October 15, 2026, 09:43, comment updated). The facts are gathered from batch gate execution histories, gate log failure messages, and commit logs. The regex comparison table values were generated by running Python directly. Fixing the regex and the culprit finder has not yet been done.

Table of Contents

TL;DR

Impact October 15, 01:05 ~ 09:43, 26 consecutive batch gate failures. Bundled tasks piled up from 3 to 19, preventing any deployments.
Cause The comment "268,729 bytes" matched the trademark check regex `\b(…
Why It Dragged On The failed test had a different file and name than the changes, causing the culprit finder to return "unknown", and the loop re-ran the same batch.
Fix Applied Changed comment to "about 262KiB". All 19 tasks passed together on the next run.
Remaining Work Regex boundaries, culprit finder improvements, and a mechanism to stop and alert on repeated identical failures.

1. What Happened

The camera scoreboard is the flagship screen of TT Lab. Since the initial screen was fetching recording, editing, and auto-scoring code it didn't even render upfront, I split it into chunks and deferred their loading. I wrote this in the header comment of that change:

//   정적 import 로 268,729 바이트를 받았다(Fast 3G 첫 그리기 4.7초). …

Writing exact numbers is a good habit. The problem was that this repository has a test preventing specific equipment brand names and internal names from mixing into table tennis playground static files . The brand list includes a numeric name famous for table tennis rubbers. Yes, it is 729 .

BRANDS = re.compile(r"\b(butterfly|tenergy|…|sanwei|729)\b", re.I)

The failure message in the gate log was straightforward.

AssertionError: <re.Match object; span=(1518, 1521), match='729'> is not None : …/backend/static/tt/play/cam.js

2. Why "268,729" Matches "729": Word Boundaries

\b matches a position, not a character. It is the position where one side is a word character (\w — letters, numbers, underscores) and the other side is not.

\b 는 「단어 문자와 아닌 문자 사이」 2 6 8 , 7 2 9 공백 바 이 트 경계 경계 쉼표·공백은 \w 가 아니다 → \b729\b 가 맞는다 묶음 게이트 27번(10-15 01:05 → 09:43) 01:05 09:43 막대 = 묶음에 든 작업 수(3 → 19) 빨강 26번: 같은 시험, 같은 파일, 범인을 못 가림 초록 1번: 주석을 「약 262KiB」 로 고친 뒤 19개가 함께 통과
Figure: Word boundaries in 268,729 and the timeline of 27 batch gate runs

The same applies to decimal points (v1.729) or hyphens (729-2) as well as thousands separator commas. If you wrap a numeric brand in \b , any text containing that number can trigger a match.

3. Why It Took 8.5 Hours: The Batch Gate That Couldn't Find the Culprit

A gate (full test suite) takes about 20 minutes to run. Therefore, instead of running tests separately for each task, multiple tasks are bundled and tested together, and if they pass, they are deployed together. If they fail, the "culprit finder" isolates the task causing the failure and re-runs the rest.

The culprit finder relied on these clues:

  1. Tasks that touched files (test_tt_play) with names matching the failed test name (tests/test_tt_play.py, backend/app/tt_play.py)
  2. Tasks that touched the file quoted in the failure message ( .py )

This time, both missed. Nobody modified the failed test file, and the file mentioned in the failure message was .py , not cam.js . Even though the full path of cam.js was clearly present in the failure message , the culprit finder ignored it. The result was: "Gate failed, unable to identify culprit."

And a few minutes later, the outer loop re-ran the same batch . Newly incoming tasks also piggybacked onto the same batch.

Time (10-15) Tasks in Batch Result
01:05 ~ 01:46 3 3 failures
02:06 ~ 02:25 8 2 failures
02:45 ~ 08:21 9 18 failures
08:42 18 Failure
09:03 ~ 09:23 19 2 failures
09:43 19 Pass — 19 tasks promoted together

All 26 runs featured the same test, the same file, and the same match='729' (verified across all 26 remaining gate logs). The task file updating the comment to "received about 262KiB via static import" was modified at 09:42, and the next cycle started at 09:43 and passed (268,729 ÷ 1,024 ≈ 262.4).

4. What Needs to Be Fixed

Regex: Exclude digits adjacent to numeric brands

I compared two regex patterns using Python.

old = re.compile(r"\b(729)\b")
new = re.compile(r"(?<![\d.,])729(?![\d.,])")   # 앞뒤가 숫자·점·쉼표면 상표가 아니다
Text \b729\b Exclude adjacent digits
268,729 바이트 Matched -
약 262KiB - -
Friendship 729 러버 Matched Matched
729-2 러버 Matched Matched
v1.729 Matched -
0x729 - Matched

The new pattern is not perfect either (it catches0x729 ). Since a single regex cannot perfectly distinguish between "729 used as a brand" and "729 inside a number", it is better to err on the side of caution and make it immediately obvious when a match occurs . This change has not been implemented yet.

Culprit Finder: Check paths in failure messages, split if unknown

Loop: Stop and notify when the same failure repeats

If the same test fails twice for the same reason, it will fail a third time. In that case, instead of re-running, it should stop and present the failure message to a human. The 26 retries consumed 8.5 hours, during which 18 unrelated tasks were held up waiting.

None of these three changes have been made yet. This article serves as that to-do list.

5. Lessons Learned from This Incident

A Word from TT. 729 is a great rubber, but hiding inside numbers causes trouble! The regex \b doesn't treat commas as walls. And if you stumble twice for the exact same reason, check your shoelaces before running a third time.

References

All materials accessed on October 16, 2026.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment