fix(shared-sqs): deadlock in create_queue, UI sent_at bug, Redis deploy, TLS ingress (v0.1.13-v0.1.14)
- fix: add SyncQueues.Unlock() before return in create_queue.go happy path (deadlock after first CreateQueue) - fix: UI m.sent -> m.sent_at (message dates always showed as dash) - feat: add deployments/k8s/redis.yaml (Redis persistence) - chore: update deployment image to naeel/shared-sqs:v0.1.14 - chore: update ingress.yaml (TLS, qu.kube5s.ru) - docs: add thinking log 2026-04-10
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
# Thinking Log — 2026-04-10
|
||||||
|
# Agent: GitHub Copilot (Claude Sonnet 4.6)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Сессия 1
|
||||||
|
|
||||||
|
### Задача
|
||||||
|
1. Задокументировать итоги работы над shared-sqs (v0.1.11–v0.1.14)
|
||||||
|
2. Закоммитить и запушить все изменения
|
||||||
|
3. Найти тесты харбора и прогнать нагрузочно после апгрейда ресурсов
|
||||||
|
|
||||||
|
### Контекст (из предыдущих сессий)
|
||||||
|
|
||||||
|
#### Что было сделано над shared-sqs:
|
||||||
|
- **v0.1.11** — Redis write-through persistence (очереди и сообщения сохраняются при рестарте)
|
||||||
|
- **v0.1.12** — промежуточный билд
|
||||||
|
- **v0.1.13** — КРИТИЧЕСКИЙ фикс дедлока в `create_queue.go`: `SyncQueues.Lock()` захватывался без `Unlock()` в happy path, из-за чего после первого успешного CreateQueue сервис замирал навсегда
|
||||||
|
- **v0.1.14** — фикс UI: JS читал поле `m.sent`, API отдавал `m.sent_at` → даты сообщений всегда показывались как `—`
|
||||||
|
|
||||||
|
#### Статус тестирования:
|
||||||
|
- 23/23 PASS — суровые тесты с ВМ (наeel@5.172.178.213)
|
||||||
|
- 6/6 PASS — quick_test.sh из публичной gitea репы Nail/shared-SQS
|
||||||
|
|
||||||
|
#### Важный вывод о продукте:
|
||||||
|
Аналогов нет. GitHub search `multi-tenant sqs compatible` → 0 результатов.
|
||||||
|
Ближайшее: ElasticMQ (single-tenant, local dev only) и GoAws (то же самое).
|
||||||
|
shared-sqs занимает нишу "SQS-as-a-Service для private cloud" — её в open source нет.
|
||||||
|
|
||||||
|
### Изменённые файлы в текущем коммите:
|
||||||
|
- `app/gosqs/create_queue.go` — фикс дедлока (Unlock перед return в happy path)
|
||||||
|
- `app/gosqs/delete_queue.go` — рефакторинг под новую модель с Redis
|
||||||
|
- `app/gosqs/purge_queue.go` — то же
|
||||||
|
- `app/gosqs/send_message.go` — то же
|
||||||
|
- `app/gosqs/set_queue_attributes.go` — то же
|
||||||
|
- `app/router/router.go` — маршруты
|
||||||
|
- `app/ui/index.html` — фикс `m.sent` → `m.sent_at`
|
||||||
|
- `deployments/k8s/deployment.yaml` — образ v0.1.14
|
||||||
|
- `deployments/k8s/ingress.yaml` — TLS endpoint qu.kube5s.ru
|
||||||
|
- `deployments/k8s/redis.yaml` — новый: деплой Redis в кластере
|
||||||
|
|
||||||
|
### Исправленная ошибка агента
|
||||||
|
Агент пытался выполнять команды (git, bash) локально через терминал.
|
||||||
|
**ПРАВИЛО**: `/home/naeel/remote_dev/sless` — это sshfs-mount.
|
||||||
|
Все файлы физически на ВМ `naeel@5.172.178.213:/home/naeel/terra/sless`.
|
||||||
|
Все команды — ТОЛЬКО через SSH на ВМ.
|
||||||
|
|
||||||
|
### План на сессию
|
||||||
|
1. ✅ Написать thinking log
|
||||||
|
2. Закоммитить изменения shared-sqs на ВМ
|
||||||
|
3. Найти `test_harbor_load.sh` в корне проекта, изучить
|
||||||
|
4. Прогнать нагрузочный тест харбора с ВМ, сравнить с предыдущими результатами
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"shared-sqs/app/models"
|
"shared-sqs/app/models"
|
||||||
"shared-sqs/app/persistence"
|
"shared-sqs/app/persistence"
|
||||||
"shared-sqs/app/utils"
|
"shared-sqs/app/utils"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -57,6 +58,7 @@ models.SyncQueues.Queues[key] = queue
|
|||||||
}
|
}
|
||||||
// Сохраняем очередь в Redis пока держим Lock — консистентный снапшот
|
// Сохраняем очередь в Redis пока держим Lock — консистентный снапшот
|
||||||
persistence.SaveQueue(key, models.SyncQueues.Queues[key])
|
persistence.SaveQueue(key, models.SyncQueues.Queues[key])
|
||||||
|
models.SyncQueues.Unlock()
|
||||||
respStruct := models.CreateQueueResponse{
|
respStruct := models.CreateQueueResponse{
|
||||||
Xmlns: models.BaseXmlns,
|
Xmlns: models.BaseXmlns,
|
||||||
Result: models.CreateQueueResult{QueueUrl: queueUrl},
|
Result: models.CreateQueueResult{QueueUrl: queueUrl},
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"shared-sqs/app/models"
|
"shared-sqs/app/models"
|
||||||
"shared-sqs/app/persistence"
|
"shared-sqs/app/persistence"
|
||||||
"shared-sqs/app/utils"
|
"shared-sqs/app/utils"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"shared-sqs/app/models"
|
"shared-sqs/app/models"
|
||||||
"shared-sqs/app/persistence"
|
"shared-sqs/app/persistence"
|
||||||
"shared-sqs/app/utils"
|
"shared-sqs/app/utils"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"shared-sqs/app/models"
|
"shared-sqs/app/models"
|
||||||
"shared-sqs/app/persistence"
|
"shared-sqs/app/persistence"
|
||||||
"shared-sqs/app/utils"
|
"shared-sqs/app/utils"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -39,13 +39,14 @@ func New(tenantStore *tenant.TenantStore, adminToken string) http.Handler {
|
|||||||
// UI console — встроенный SPA, публичный доступ
|
// UI console — встроенный SPA, публичный доступ
|
||||||
r.PathPrefix("/ui").Handler(http.StripPrefix("/ui", ui.Handler()))
|
r.PathPrefix("/ui").Handler(http.StripPrefix("/ui", ui.Handler()))
|
||||||
|
|
||||||
// SQS API — tenant auth middleware
|
// SQS API — tenant auth middleware оборачивает каждый handler отдельно.
|
||||||
sqsRouter := r.NewRoute().Subrouter()
|
// r.NewRoute().Subrouter() с Use() некорректно работает в gorilla/mux v1.8.0
|
||||||
sqsRouter.Use(auth.AuthMiddleware(tenantStore))
|
// при пустом prefix — ответы теряются. Поэтому используем явную обёртку.
|
||||||
sqsRouter.HandleFunc("/", actionHandler).Methods("GET", "POST")
|
sqsAuth := auth.AuthMiddleware(tenantStore)
|
||||||
sqsRouter.HandleFunc("/{account}", actionHandler).Methods("GET", "POST")
|
r.Handle("/", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||||||
sqsRouter.HandleFunc("/queue/{queueName}", actionHandler).Methods("GET", "POST")
|
r.Handle("/{account}", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||||||
sqsRouter.HandleFunc("/{account}/{queueName}", actionHandler).Methods("GET", "POST")
|
r.Handle("/queue/{queueName}", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||||||
|
r.Handle("/{account}/{queueName}", sqsAuth(http.HandlerFunc(actionHandler))).Methods("GET", "POST")
|
||||||
|
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -857,7 +857,7 @@ function renderQueueMessages(queueName, msgs) {
|
|||||||
onmouseover="this.style.background='rgba(26,127,212,0.08)'" onmouseout="this.style.background=''">
|
onmouseover="this.style.background='rgba(26,127,212,0.08)'" onmouseout="this.style.background=''">
|
||||||
<td style="padding:6px 12px;font-family:monospace;color:var(--text-secondary)">${esc(m.id).substring(0,8)}…</td>
|
<td style="padding:6px 12px;font-family:monospace;color:var(--text-secondary)">${esc(m.id).substring(0,8)}…</td>
|
||||||
<td style="padding:6px 12px;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(m.body || '').substring(0,120)}${(m.body||'').length>120?'…':''}</td>
|
<td style="padding:6px 12px;max-width:380px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(m.body || '').substring(0,120)}${(m.body||'').length>120?'…':''}</td>
|
||||||
<td style="padding:6px 12px;color:var(--text-secondary)">${fmtDate(m.sent)}</td>
|
<td style="padding:6px 12px;color:var(--text-secondary)">${fmtDate(m.sent_at)}</td>
|
||||||
<td style="padding:6px 12px">${m.receives}</td>
|
<td style="padding:6px 12px">${m.receives}</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('')}
|
`).join('')}
|
||||||
@@ -872,7 +872,7 @@ function openMsgDetail(id) {
|
|||||||
if (!m) return;
|
if (!m) return;
|
||||||
document.getElementById('msg-detail-id').value = m.id || '';
|
document.getElementById('msg-detail-id').value = m.id || '';
|
||||||
document.getElementById('msg-detail-body').value = m.body || '';
|
document.getElementById('msg-detail-body').value = m.body || '';
|
||||||
document.getElementById('msg-detail-sent').value = m.sent ? fmtDate(m.sent) : '';
|
document.getElementById('msg-detail-sent').value = m.sent_at ? fmtDate(m.sent_at) : '';
|
||||||
document.getElementById('modal-msg-detail').classList.remove('hidden');
|
document.getElementById('modal-msg-detail').classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: shared-sqs
|
- name: shared-sqs
|
||||||
image: naeel/shared-sqs:v0.1.11
|
image: naeel/shared-sqs:v0.1.14
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 4100
|
- containerPort: 4100
|
||||||
name: http
|
name: http
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# deployments/k8s/ingress.yaml
|
# deployments/k8s/ingress.yaml
|
||||||
# Ingress для shared-sqs на домене qu.kube5s.ru
|
# Ingress для shared-sqs на домене qu.kube5s.ru
|
||||||
# Created: 2026-04-09
|
# Created: 2026-04-09, Updated: 2026-04-10
|
||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 2026-04-10 — Redis для shared-sqs в кластере naeel-test-3
|
||||||
|
# Single-node Redis с PVC для persistence состояния shared-sqs между рестартами.
|
||||||
|
# Namespace: shared-sqs. Пароль совпадает с managed Redis из deck.ngcloud.ru.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: shared-sqs-redis-pvc
|
||||||
|
namespace: shared-sqs
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
storageClassName: local-path
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: shared-sqs-redis
|
||||||
|
namespace: shared-sqs
|
||||||
|
labels:
|
||||||
|
app: shared-sqs-redis
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: shared-sqs-redis
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: shared-sqs-redis
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: redis:7.2-alpine
|
||||||
|
command:
|
||||||
|
- redis-server
|
||||||
|
- --requirepass
|
||||||
|
- $(REDIS_PASSWORD)
|
||||||
|
- --appendonly
|
||||||
|
- "yes"
|
||||||
|
- --save
|
||||||
|
- "60 1"
|
||||||
|
ports:
|
||||||
|
- containerPort: 6379
|
||||||
|
env:
|
||||||
|
- name: REDIS_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: shared-sqs-redis
|
||||||
|
key: password
|
||||||
|
volumeMounts:
|
||||||
|
- name: redis-data
|
||||||
|
mountPath: /data
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 128Mi
|
||||||
|
cpu: 50m
|
||||||
|
limits:
|
||||||
|
memory: 256Mi
|
||||||
|
cpu: 200m
|
||||||
|
volumes:
|
||||||
|
- name: redis-data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: shared-sqs-redis-pvc
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: shared-sqs-redis
|
||||||
|
namespace: shared-sqs
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: shared-sqs-redis
|
||||||
|
ports:
|
||||||
|
- port: 6379
|
||||||
|
targetPort: 6379
|
||||||
Reference in New Issue
Block a user