From 040c8df02ed8235661138d4fdd50bc701662745b Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:00:02 +0800 Subject: [PATCH] fix service api (#556) * fix service api * update ServiceResponseItem * fix: ddns lock --- cmd/dashboard/controller/controller.go | 3 +- cmd/dashboard/controller/ddns.go | 4 +- cmd/dashboard/controller/nat.go | 4 +- cmd/dashboard/controller/notification.go | 4 +- cmd/dashboard/controller/service.go | 59 ++++++++++------ model/service_api.go | 2 +- service/singleton/ddns.go | 4 ++ service/singleton/nat.go | 4 ++ service/singleton/notification.go | 8 ++- service/singleton/servicesentinel.go | 85 +++++++++++++++++------- 10 files changed, 124 insertions(+), 53 deletions(-) diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 0bd8109..52bc452 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -60,7 +60,7 @@ func routers(r *gin.Engine, adminFrontend, userFrontend fs.FS) { optionalAuth.GET("/ws/server", commonHandler(serverStream)) optionalAuth.GET("/server-group", commonHandler(listServerGroup)) - optionalAuth.GET("/service", commonHandler(listService)) + optionalAuth.GET("/service", commonHandler(showService)) optionalAuth.GET("/service/:id", commonHandler(listServiceHistory)) optionalAuth.GET("/service/server", commonHandler(listServerWithServices)) @@ -82,6 +82,7 @@ func routers(r *gin.Engine, adminFrontend, userFrontend fs.FS) { auth.POST("/user", commonHandler(createUser)) auth.POST("/batch-delete/user", commonHandler(batchDeleteUser)) + auth.GET("/service/list", commonHandler(listService)) auth.POST("/service", commonHandler(createService)) auth.PATCH("/service/:id", commonHandler(updateService)) auth.POST("/batch-delete/service", commonHandler(batchDeleteService)) diff --git a/cmd/dashboard/controller/ddns.go b/cmd/dashboard/controller/ddns.go index adf2b15..b58c28e 100644 --- a/cmd/dashboard/controller/ddns.go +++ b/cmd/dashboard/controller/ddns.go @@ -23,8 +23,8 @@ import ( func listDDNS(c *gin.Context) ([]*model.DDNSProfile, error) { var ddnsProfiles []*model.DDNSProfile - singleton.DDNSCacheLock.RLock() - defer singleton.DDNSCacheLock.RUnlock() + singleton.DDNSListLock.RLock() + defer singleton.DDNSListLock.RUnlock() if err := copier.Copy(&ddnsProfiles, &singleton.DDNSList); err != nil { return nil, err diff --git a/cmd/dashboard/controller/nat.go b/cmd/dashboard/controller/nat.go index 4477a47..6a2737e 100644 --- a/cmd/dashboard/controller/nat.go +++ b/cmd/dashboard/controller/nat.go @@ -22,8 +22,8 @@ import ( func listNAT(c *gin.Context) ([]*model.NAT, error) { var n []*model.NAT - singleton.NATCacheRwLock.RLock() - defer singleton.NATCacheRwLock.RUnlock() + singleton.NATListLock.RLock() + defer singleton.NATListLock.RUnlock() if err := copier.Copy(&n, &singleton.NATList); err != nil { return nil, err diff --git a/cmd/dashboard/controller/notification.go b/cmd/dashboard/controller/notification.go index e7c2ea7..78ac770 100644 --- a/cmd/dashboard/controller/notification.go +++ b/cmd/dashboard/controller/notification.go @@ -20,8 +20,8 @@ import ( // @Success 200 {object} model.CommonResponse[[]model.Notification] // @Router /notification [get] func listNotification(c *gin.Context) ([]*model.Notification, error) { - singleton.NotificationsLock.RLock() - defer singleton.NotificationsLock.RUnlock() + singleton.NotificationSortedLock.RLock() + defer singleton.NotificationSortedLock.RUnlock() var notifications []*model.Notification if err := copier.Copy(¬ifications, &singleton.NotificationListSorted); err != nil { diff --git a/cmd/dashboard/controller/service.go b/cmd/dashboard/controller/service.go index 435c979..2956681 100644 --- a/cmd/dashboard/controller/service.go +++ b/cmd/dashboard/controller/service.go @@ -13,35 +13,22 @@ import ( "gorm.io/gorm" ) -// List service -// @Summary List service +// Show service +// @Summary Show service // @Security BearerAuth // @Schemes -// @Description List service +// @Description Show service // @Tags common // @Produce json // @Success 200 {object} model.CommonResponse[model.ServiceResponse] // @Router /service [get] -func listService(c *gin.Context) (*model.ServiceResponse, error) { +func showService(c *gin.Context) (*model.ServiceResponse, error) { res, err, _ := requestGroup.Do("list-service", func() (interface{}, error) { singleton.AlertsLock.RLock() defer singleton.AlertsLock.RUnlock() - var stats map[uint64]model.ServiceResponseItem + stats := singleton.ServiceSentinelShared.CopyStats() var cycleTransferStats map[uint64]model.CycleTransferStats - copier.Copy(&stats, singleton.ServiceSentinelShared.LoadStats()) copier.Copy(&cycleTransferStats, singleton.AlertsCycleTransferStatsStore) - _, isMember := c.Get(model.CtxKeyAuthorizedUser) - authorized := isMember // TODO || isViewPasswordVerfied - for k, service := range stats { - if !authorized { - if !service.Service.EnableShowInService { - delete(stats, k) - continue - } - service.Service = &model.Service{Name: service.Service.Name} - stats[k] = service - } - } return []interface { }{ stats, cycleTransferStats, @@ -57,6 +44,27 @@ func listService(c *gin.Context) (*model.ServiceResponse, error) { }, nil } +// List service +// @Summary List service +// @Security BearerAuth +// @Schemes +// @Description List service +// @Tags auth required +// @Produce json +// @Success 200 {object} model.CommonResponse[[]model.Service] +// @Router /service [get] +func listService(c *gin.Context) ([]*model.Service, error) { + singleton.ServiceSentinelShared.ServicesLock.RLock() + defer singleton.ServiceSentinelShared.ServicesLock.RUnlock() + + var ss []*model.Service + if err := copier.Copy(&ss, singleton.ServiceSentinelShared.ServiceList); err != nil { + return nil, err + } + + return ss, nil +} + // List service histories by server id // @Summary List service histories by server id // @Security BearerAuth @@ -218,7 +226,12 @@ func createService(c *gin.Context) (uint64, error) { return 0, err } - return m.ID, singleton.ServiceSentinelShared.OnServiceUpdate(m) + if err := singleton.ServiceSentinelShared.OnServiceUpdate(m); err != nil { + return 0, err + } + + singleton.ServiceSentinelShared.UpdateServiceList() + return m.ID, nil } // Update service @@ -281,7 +294,12 @@ func updateService(c *gin.Context) (any, error) { return nil, err } - return nil, singleton.ServiceSentinelShared.OnServiceUpdate(m) + if err := singleton.ServiceSentinelShared.OnServiceUpdate(m); err != nil { + return nil, err + } + + singleton.ServiceSentinelShared.UpdateServiceList() + return nil, nil } // Batch delete service @@ -310,5 +328,6 @@ func batchDeleteService(c *gin.Context) (any, error) { return nil, err } singleton.ServiceSentinelShared.OnServiceDelete(ids) + singleton.ServiceSentinelShared.UpdateServiceList() return nil, nil } diff --git a/model/service_api.go b/model/service_api.go index c52b6f9..ceb78cb 100644 --- a/model/service_api.go +++ b/model/service_api.go @@ -21,7 +21,7 @@ type ServiceForm struct { } type ServiceResponseItem struct { - Service *Service `json:"service,omitempty"` + ServiceName string `json:"service_name,omitempty"` CurrentUp uint64 `json:"current_up"` CurrentDown uint64 `json:"current_down"` TotalUp uint64 `json:"total_up"` diff --git a/service/singleton/ddns.go b/service/singleton/ddns.go index b90084a..e2799fb 100644 --- a/service/singleton/ddns.go +++ b/service/singleton/ddns.go @@ -19,6 +19,7 @@ var ( DDNSCache map[uint64]*model.DDNSProfile DDNSCacheLock sync.RWMutex DDNSList []*model.DDNSProfile + DDNSListLock sync.RWMutex ) func initDDNS() { @@ -52,6 +53,9 @@ func UpdateDDNSList() { DDNSCacheLock.RLock() defer DDNSCacheLock.RUnlock() + DDNSListLock.Lock() + defer DDNSListLock.Unlock() + DDNSList = make([]*model.DDNSProfile, 0, len(DDNSCache)) for _, p := range DDNSCache { DDNSList = append(DDNSList, p) diff --git a/service/singleton/nat.go b/service/singleton/nat.go index bc7fccb..f03bc04 100644 --- a/service/singleton/nat.go +++ b/service/singleton/nat.go @@ -14,6 +14,7 @@ var ( NATIDToDomain = make(map[uint64]string) NATList []*model.NAT + NATListLock sync.RWMutex ) func initNAT() { @@ -55,6 +56,9 @@ func UpdateNATList() { NATCacheRwLock.RLock() defer NATCacheRwLock.RUnlock() + NATListLock.Lock() + defer NATListLock.Unlock() + NATList = make([]*model.NAT, 0, len(NATCache)) for _, n := range NATCache { NATList = append(NATList, n) diff --git a/service/singleton/notification.go b/service/singleton/notification.go index 4bddce3..3b26bbc 100644 --- a/service/singleton/notification.go +++ b/service/singleton/notification.go @@ -24,8 +24,9 @@ var ( NotificationListSorted []*model.Notification NotificationGroup map[uint64]string // [NotificationGroupID] -> [NotificationGroupName] - NotificationsLock sync.RWMutex - NotificationGroupLock sync.RWMutex + NotificationsLock sync.RWMutex + NotificationSortedLock sync.RWMutex + NotificationGroupLock sync.RWMutex ) // InitNotification 初始化 GroupID <-> ID <-> Notification 的映射 @@ -81,6 +82,9 @@ func UpdateNotificationList() { NotificationsLock.RLock() defer NotificationsLock.RUnlock() + NotificationSortedLock.Lock() + defer NotificationSortedLock.Unlock() + NotificationListSorted = make([]*model.Notification, 0, len(NotificationMap)) for _, n := range NotificationMap { NotificationListSorted = append(NotificationListSorted, n) diff --git a/service/singleton/servicesentinel.go b/service/singleton/servicesentinel.go index 7951734..4638000 100644 --- a/service/singleton/servicesentinel.go +++ b/service/singleton/servicesentinel.go @@ -3,12 +3,14 @@ package singleton import ( "fmt" "log" - "sort" + "slices" "strings" "sync" "time" + "github.com/jinzhu/copier" "github.com/nezhahq/nezha/model" + "github.com/nezhahq/nezha/pkg/utils" pb "github.com/nezhahq/nezha/proto" ) @@ -18,6 +20,12 @@ const ( var ServiceSentinelShared *ServiceSentinel +type serviceResponseItem struct { + model.ServiceResponseItem + + service *model.Service +} + type ReportData struct { Data *pb.TaskResult Reporter uint64 @@ -45,7 +53,7 @@ func NewServiceSentinel(serviceSentinelDispatchBus chan<- model.Service) { Services: make(map[uint64]*model.Service), tlsCertCache: make(map[uint64]string), // 30天数据缓存 - monthlyStatus: make(map[uint64]*model.ServiceResponseItem), + monthlyStatus: make(map[uint64]*serviceResponseItem), dispatchBus: serviceSentinelDispatchBus, } // 加载历史记录 @@ -104,12 +112,14 @@ type ServiceSentinel struct { lastStatus map[uint64]int tlsCertCache map[uint64]string - ServicesLock sync.RWMutex - Services map[uint64]*model.Service + ServicesLock sync.RWMutex + ServiceListLock sync.RWMutex + Services map[uint64]*model.Service + ServiceList []*model.Service // 30天数据缓存 monthlyStatusLock sync.Mutex - monthlyStatus map[uint64]*model.ServiceResponseItem + monthlyStatus map[uint64]*serviceResponseItem } type indexStore struct { @@ -157,17 +167,21 @@ func (ss *ServiceSentinel) Dispatch(r ReportData) { ss.serviceReportChannel <- r } -func (ss *ServiceSentinel) GetServiceList() []*model.Service { +func (ss *ServiceSentinel) UpdateServiceList() { ss.ServicesLock.RLock() defer ss.ServicesLock.RUnlock() - var services []*model.Service + + ss.ServiceListLock.Lock() + defer ss.ServiceListLock.Unlock() + + ss.ServiceList = make([]*model.Service, 0, len(ss.Services)) for _, v := range ss.Services { - services = append(services, v) + ss.ServiceList = append(ss.ServiceList, v) } - sort.SliceStable(services, func(i, j int) bool { - return services[i].ID < services[j].ID + + slices.SortFunc(ss.ServiceList, func(a, b *model.Service) int { + return utils.Compare(a.ID, b.ID) }) - return services } // loadServiceHistory 加载服务监控器的历史状态信息 @@ -198,16 +212,19 @@ func (ss *ServiceSentinel) loadServiceHistory() { ss.serviceCurrentStatusData[services[i].ID] = make([]*pb.TaskResult, _CurrentStatusSize) ss.serviceStatusToday[services[i].ID] = &_TodayStatsOfService{} } + ss.ServiceList = services year, month, day := time.Now().Date() today := time.Date(year, month, day, 0, 0, 0, 0, Loc) for i := 0; i < len(services); i++ { - ServiceSentinelShared.monthlyStatus[services[i].ID] = &model.ServiceResponseItem{ - Service: services[i], - Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Down: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + ServiceSentinelShared.monthlyStatus[services[i].ID] = &serviceResponseItem{ + service: services[i], + ServiceResponseItem: model.ServiceResponseItem{ + Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Down: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, } } @@ -250,11 +267,13 @@ func (ss *ServiceSentinel) OnServiceUpdate(m model.Service) error { Cron.Remove(ss.Services[m.ID].CronJobID) } else { // 新任务初始化数据 - ss.monthlyStatus[m.ID] = &model.ServiceResponseItem{ - Service: &m, - Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - Down: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + ss.monthlyStatus[m.ID] = &serviceResponseItem{ + service: &m, + ServiceResponseItem: model.ServiceResponseItem{ + Delay: &[30]float32{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Up: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Down: &[30]int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + }, } ss.serviceCurrentStatusData[m.ID] = make([]*pb.TaskResult, _CurrentStatusSize) ss.serviceStatusToday[m.ID] = &_TodayStatsOfService{} @@ -290,7 +309,9 @@ func (ss *ServiceSentinel) OnServiceDelete(ids []uint64) { } } -func (ss *ServiceSentinel) LoadStats() map[uint64]*model.ServiceResponseItem { +func (ss *ServiceSentinel) LoadStats() map[uint64]*serviceResponseItem { + ss.ServicesLock.RLock() + defer ss.ServicesLock.RUnlock() ss.serviceResponseDataStoreLock.RLock() defer ss.serviceResponseDataStoreLock.RUnlock() ss.monthlyStatusLock.Lock() @@ -298,7 +319,7 @@ func (ss *ServiceSentinel) LoadStats() map[uint64]*model.ServiceResponseItem { // 刷新最新一天的数据 for k := range ss.Services { - ss.monthlyStatus[k].Service = ss.Services[k] + ss.monthlyStatus[k].service = ss.Services[k] v := ss.serviceStatusToday[k] // 30 天在线率, @@ -325,6 +346,24 @@ func (ss *ServiceSentinel) LoadStats() map[uint64]*model.ServiceResponseItem { return ss.monthlyStatus } +func (ss *ServiceSentinel) CopyStats() map[uint64]model.ServiceResponseItem { + var stats map[uint64]*serviceResponseItem + copier.Copy(&stats, ss.LoadStats()) + + sri := make(map[uint64]model.ServiceResponseItem) + for k, service := range stats { + if !service.service.EnableShowInService { + delete(stats, k) + continue + } + + service.ServiceName = service.service.Name + sri[k] = service.ServiceResponseItem + } + + return sri +} + // worker 服务监控的实际工作流程 func (ss *ServiceSentinel) worker() { // 从服务状态汇报管道获取汇报的服务数据