github.com/gnolang/gno@v0.0.0-20240520182011-228e9d0192ce/tm2/pkg/service/service.go (about) 1 package service 2 3 import ( 4 "errors" 5 "fmt" 6 "log/slog" 7 "sync/atomic" 8 9 "github.com/gnolang/gno/tm2/pkg/log" 10 ) 11 12 var ( 13 // ErrAlreadyStarted is returned when somebody tries to start an already 14 // running service. 15 ErrAlreadyStarted = errors.New("already started") 16 // ErrAlreadyStopped is returned when somebody tries to stop an already 17 // stopped service (without resetting it). 18 ErrAlreadyStopped = errors.New("already stopped") 19 // ErrNotStarted is returned when somebody tries to stop a not running 20 // service. 21 ErrNotStarted = errors.New("not started") 22 ) 23 24 // Service defines a service that can be started, stopped, and reset. 25 type Service interface { 26 // Start the service. 27 // If it's already started or stopped, will return an error. 28 // If OnStart() returns an error, it's returned by Start() 29 Start() error 30 OnStart() error 31 32 // Stop the service. 33 // If it's already stopped, will return an error. 34 // OnStop must never error. 35 Stop() error 36 OnStop() 37 38 // Reset the service. 39 // Panics by default - must be overwritten to enable reset. 40 Reset() error 41 OnReset() error 42 43 // Return true if the service is running 44 IsRunning() bool 45 46 // Quit returns a channel, which is closed once service is stopped. 47 Quit() <-chan struct{} 48 49 // String representation of the service 50 String() string 51 52 // SetLogger sets a logger. 53 SetLogger(*slog.Logger) 54 } 55 56 /* 57 Classical-inheritance-style service declarations. Services can be started, then 58 stopped, then optionally restarted. 59 60 Users can override the OnStart/OnStop methods. In the absence of errors, these 61 methods are guaranteed to be called at most once. If OnStart returns an error, 62 service won't be marked as started, so the user can call Start again. 63 64 A call to Reset will panic, unless OnReset is overwritten, allowing 65 OnStart/OnStop to be called again. 66 67 The caller must ensure that Start and Stop are not called concurrently. 68 69 It is ok to call Stop without calling Start first. 70 71 Typical usage: 72 73 type FooService struct { 74 BaseService 75 // private fields 76 } 77 78 func NewFooService() *FooService { 79 fs := &FooService{ 80 // init 81 } 82 fs.BaseService = *NewBaseService(log, "FooService", fs) 83 return fs 84 } 85 86 func (fs *FooService) OnStart() error { 87 fs.BaseService.OnStart() // Always call the overridden method. 88 // initialize private fields 89 // start subroutines, etc. 90 } 91 92 func (fs *FooService) OnStop() error { 93 fs.BaseService.OnStop() // Always call the overridden method. 94 // close/destroy private fields 95 // stop subroutines, etc. 96 } 97 */ 98 type BaseService struct { 99 Logger *slog.Logger 100 name string 101 started uint32 // atomic 102 stopped uint32 // atomic 103 quit chan struct{} 104 105 // The "subclass" of BaseService 106 impl Service 107 } 108 109 // NewBaseService creates a new BaseService. 110 func NewBaseService(logger *slog.Logger, name string, impl Service) *BaseService { 111 if logger == nil { 112 logger = log.NewNoopLogger() 113 } 114 115 return &BaseService{ 116 Logger: logger, 117 name: name, 118 quit: make(chan struct{}), 119 impl: impl, 120 } 121 } 122 123 // SetLogger implements Service by setting a logger. 124 func (bs *BaseService) SetLogger(l *slog.Logger) { 125 bs.Logger = l 126 } 127 128 // Start implements Service by calling OnStart (if defined). An error will be 129 // returned if the service is already running or stopped. Not to start the 130 // stopped service, you need to call Reset. 131 func (bs *BaseService) Start() error { 132 if atomic.CompareAndSwapUint32(&bs.started, 0, 1) { 133 if atomic.LoadUint32(&bs.stopped) == 1 { 134 bs.Logger.Error(fmt.Sprintf("Not starting %v -- already stopped", bs.name), "impl", bs.impl) 135 // revert flag 136 atomic.StoreUint32(&bs.started, 0) 137 return ErrAlreadyStopped 138 } 139 bs.Logger.Info(fmt.Sprintf("Starting %v", bs.name), "impl", bs.impl) 140 err := bs.impl.OnStart() 141 if err != nil { 142 // revert flag 143 atomic.StoreUint32(&bs.started, 0) 144 return err 145 } 146 return nil 147 } 148 bs.Logger.Debug(fmt.Sprintf("Not starting %v -- already started", bs.name), "impl", bs.impl) 149 return ErrAlreadyStarted 150 } 151 152 // OnStart implements Service by doing nothing. 153 // NOTE: Do not put anything in here, 154 // that way users don't need to call BaseService.OnStart() 155 func (bs *BaseService) OnStart() error { return nil } 156 157 // Stop implements Service by calling OnStop (if defined) and closing quit 158 // channel. An error will be returned if the service is already stopped. 159 func (bs *BaseService) Stop() error { 160 if atomic.CompareAndSwapUint32(&bs.stopped, 0, 1) { 161 if atomic.LoadUint32(&bs.started) == 0 { 162 bs.Logger.Error(fmt.Sprintf("Not stopping %v -- have not been started yet", bs.name), "impl", bs.impl) 163 // revert flag 164 atomic.StoreUint32(&bs.stopped, 0) 165 return ErrNotStarted 166 } 167 bs.Logger.Info(fmt.Sprintf("Stopping %v", bs.name), "impl", bs.impl) 168 bs.impl.OnStop() 169 close(bs.quit) 170 return nil 171 } 172 bs.Logger.Debug(fmt.Sprintf("Stopping %v (ignoring: already stopped)", bs.name), "impl", bs.impl) 173 return ErrAlreadyStopped 174 } 175 176 // OnStop implements Service by doing nothing. 177 // NOTE: Do not put anything in here, 178 // that way users don't need to call BaseService.OnStop() 179 func (bs *BaseService) OnStop() {} 180 181 // Reset implements Service by calling OnReset callback (if defined). An error 182 // will be returned if the service is running. 183 func (bs *BaseService) Reset() error { 184 if !atomic.CompareAndSwapUint32(&bs.stopped, 1, 0) { 185 bs.Logger.Debug(fmt.Sprintf("Can't reset %v. Not stopped", bs.name), "impl", bs.impl) 186 return fmt.Errorf("can't reset running %s", bs.name) 187 } 188 189 // whether or not we've started, we can reset 190 atomic.CompareAndSwapUint32(&bs.started, 1, 0) 191 192 bs.quit = make(chan struct{}) 193 return bs.impl.OnReset() 194 } 195 196 // OnReset implements Service by panicking. 197 func (bs *BaseService) OnReset() error { 198 panic("The service cannot be reset") 199 } 200 201 // IsRunning implements Service by returning true or false depending on the 202 // service's state. 203 func (bs *BaseService) IsRunning() bool { 204 return atomic.LoadUint32(&bs.started) == 1 && atomic.LoadUint32(&bs.stopped) == 0 205 } 206 207 // Wait blocks until the service is stopped. 208 func (bs *BaseService) Wait() { 209 <-bs.quit 210 } 211 212 // String implements Service by returning a string representation of the service. 213 func (bs *BaseService) String() string { 214 return bs.name 215 } 216 217 // Quit Implements Service by returning a quit channel. 218 func (bs *BaseService) Quit() <-chan struct{} { 219 return bs.quit 220 }