fix(event): add 401 handling to prevent infinite retry loop on auth failure

- UploadError struct to distinguish fatal auth errors from network errors
- Clear buffer and throttle on 401/403 to save bandwidth
- Prevent dead-loop retry when token is invalid or expired
This commit is contained in:
2026-05-08 21:00:14 +08:00
parent 78427b2e33
commit 88b2e71a77
6 changed files with 174 additions and 9 deletions

11
.dockerignore Normal file
View File

@@ -0,0 +1,11 @@
.git/
.gitignore
build/
data/
*.om
*.onnx
*.tar.gz
logs/
venv/
__pycache__/
*.pyc

38
Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# 使用华为云镜像加速的 Python 3.9 Slim
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.9-slim
ENV DEBIAN_FRONTEND=noninteractive
# 安装 ffmpeg, curl 及 OpenCV 基础依赖 (使用华为云 apt 源)
RUN sed -i 's/deb.debian.org/repo.huaweicloud.com/g' /etc/apt/sources.list && \
sed -i 's/security.debian.org/repo.huaweicloud.com/g' /etc/apt/sources.list && \
apt-get update && apt-get install -y --no-install-recommends \
ffmpeg curl libgl1 libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/tianyan
# 1. 复制已编译的 Go 二进制 (由宿主机构建)
COPY build/edge-agent /opt/tianyan/edge-agent
# 2. 安装 Python 依赖 (使用华为云 pip 源)
COPY python/requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt -i https://repo.huaweicloud.com/repository/pypi/simple \
&& rm -rf /root/.cache/pip
# 3. 复制业务代码与脚本
COPY python/ /opt/tianyan/python/
COPY scripts/ /opt/tianyan/scripts/
# 4. 创建运行时目录
RUN mkdir -p /opt/tianyan/config /opt/tianyan/model /tmp
# 环境变量 (CANN 路径将由 docker-compose 挂载覆盖)
ENV PYTHONPATH=/usr/local/Ascend/ascend-toolkit/latest/python/site-packages
ENV LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/lib64:/usr/local/Ascend/driver/lib64
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 启动脚本
COPY entrypoint.sh /opt/tianyan/entrypoint.sh
RUN chmod +x /opt/tianyan/entrypoint.sh
ENTRYPOINT ["/opt/tianyan/entrypoint.sh"]

16
data/config/edge.yaml Normal file
View File

@@ -0,0 +1,16 @@
device_uuid: 8541db9f77826e39605ef2c032f8fb93
edge_id: edge-demo-001
cloud_url: http://101.36.73.102:8004
mqtt_broker: tcp://101.36.73.102:1883
mqtt_user: ""
mqtt_pass: ""
edge_token: "be079dc5ec8d7c3796d052f3c10143cd"
rtsp_urls:
- "http://101.36.73.102:8080/rtp/34020000002000000003_34020000001310000001.live.flv?secret=be079dc5ec8d7c3796d052f3c10143cd"
infer_socket: /tmp/edge-infer.sock
infer_fps: 2
infer_workers: 3
conf_threshold: 0.2
dedup_window_sec: 30
ota_url: http://101.36.73.102:8087
version: 1.0.0

34
docker-compose.yml Normal file
View File

@@ -0,0 +1,34 @@
version: '3.8'
services:
tianyan-edge:
build:
context: .
dockerfile: Dockerfile
image: tianyan-edge:latest
container_name: tianyan-edge
restart: unless-stopped
network_mode: "host"
pid: "host"
# 显式映射 NPU 设备
devices:
- /dev/davinci0
- /dev/davinci_manager
- /dev/devmm_svm
- /dev/hisi_hdc
volumes:
# 🔑 核心:挂载宿主机 CANN 驱动与库
- /usr/local/Ascend:/usr/local/Ascend:ro
# 挂载配置与模型
- ./data/config:/opt/tianyan/config
- ./data/model:/opt/tianyan/model
# 共享 Unix Socket 与日志
- /tmp:/tmp
- ./data/logs:/var/log/tianyan
environment:
- TZ=Asia/Shanghai

41
entrypoint.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e
echo "🚀 Starting Tianyan Edge Container..."
# 1. 启动 NPU 推理服务 (后台)
echo "[1/2] Starting infer_server.py..."
cd /opt/tianyan
python3 python/infer_server.py > /var/log/tianyan/infer.log 2>&1 &
INFER_PID=$!
# 2. 等待 Unix Socket 创建 (最多 20s)
echo "⏳ Waiting for inference socket at $INFER_SOCKET..."
for i in $(seq 1 20); do
if [ -S "${INFER_SOCKET:-/tmp/edge-infer.sock}" ]; then
echo "✅ Socket ready. Starting edge-agent..."
break
fi
sleep 1
done
if [ ! -S "${INFER_SOCKET:-/tmp/edge-infer.sock}" ]; then
echo "❌ ERROR: Inference socket not created. Check NPU/CANN status."
cat /var/log/tianyan/infer.log
kill $INFER_PID 2>/dev/null
exit 1
fi
# 3. 启动 Go 边缘代理
/opt/tianyan/edge-agent -config /opt/tianyan/config/edge.yaml > /var/log/tianyan/agent.log 2>&1 &
AGENT_PID=$!
# 4. 信号捕获与优雅退出
trap "echo 'Shutting down...'; kill $INFER_PID $AGENT_PID 2>/dev/null; wait; exit 0" SIGINT SIGTERM
# 阻塞主进程
wait -n
EXIT_CODE=$?
echo "Process exited with code $EXIT_CODE"
kill $INFER_PID $AGENT_PID 2>/dev/null
exit $EXIT_CODE

View File

@@ -33,8 +33,15 @@ func (u *Uploader) Run(ctx context.Context) {
case <-ctx.Done(): case <-ctx.Done():
return return
case ev := <-u.events: case ev := <-u.events:
if !u.upload(client, ev) { if err := u.upload(client, ev); err != nil {
u.buffer(ev) if ue, ok := err.(*UploadError); ok && ue.Fatal {
log.Printf("uploader: FATAL: %s. Clearing buffer and stopping.", ue.Msg)
u.buf = nil // Drop all pending events
// Give some time before returning or loop with delay
time.Sleep(1 * time.Minute)
} else {
u.buffer(ev)
}
} }
case <-ticker.C: case <-ticker.C:
u.flushBuffer(client) u.flushBuffer(client)
@@ -53,14 +60,26 @@ func (u *Uploader) buffer(ev SuspectedEvent) {
func (u *Uploader) flushBuffer(client *http.Client) { func (u *Uploader) flushBuffer(client *http.Client) {
remaining := u.buf[:0] remaining := u.buf[:0]
for _, ev := range u.buf { for _, ev := range u.buf {
if !u.upload(client, ev) { if err := u.upload(client, ev); err != nil {
if ue, ok := err.(*UploadError); ok && ue.Fatal {
log.Printf("uploader: FATAL flush: %s. Dropping remaining buffer.", ue.Msg)
u.buf = nil
return
}
remaining = append(remaining, ev) remaining = append(remaining, ev)
} }
} }
u.buf = remaining u.buf = remaining
} }
func (u *Uploader) upload(client *http.Client, ev SuspectedEvent) bool { type UploadError struct {
Fatal bool
Msg string
}
func (e *UploadError) Error() string { return e.Msg }
func (u *Uploader) upload(client *http.Client, ev SuspectedEvent) error {
url := fmt.Sprintf("%s/api/v1/edge/events/suspected", u.cfg.CloudURL) url := fmt.Sprintf("%s/api/v1/edge/events/suspected", u.cfg.CloudURL)
body, _ := json.Marshal(ev) body, _ := json.Marshal(ev)
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) req, _ := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
@@ -69,12 +88,18 @@ func (u *Uploader) upload(client *http.Client, ev SuspectedEvent) bool {
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
log.Printf("uploader: upload error: %v", err) log.Printf("uploader: upload error: %v", err)
return false return nil // Network error, retryable
} }
defer resp.Body.Close() defer resp.Body.Close()
ok := resp.StatusCode == 200 || resp.StatusCode == 201
if !ok { if resp.StatusCode == 401 || resp.StatusCode == 403 {
log.Printf("uploader: upload failed status=%d", resp.StatusCode) return &UploadError{Fatal: true, Msg: fmt.Sprintf("unauthorized (status %d), check token", resp.StatusCode)}
} }
return ok
if resp.StatusCode == 200 || resp.StatusCode == 201 {
return nil
}
log.Printf("uploader: upload failed status=%d", resp.StatusCode)
return nil // Server error, retryable
} }