├── go.mod ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cyclicbarrier.go └── cyclicbarrier_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/marusama/cyclicbarrier 2 | 3 | go 1.11 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | - 1.14.x 6 | - 1.x 7 | - master 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | install: 14 | - go get golang.org/x/tools/cmd/cover 15 | - go get github.com/mattn/goveralls 16 | 17 | script: 18 | - go test -v -covermode=count -coverprofile=coverage.out 19 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 marusama 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cyclicbarrier 2 | ============= 3 | [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go#goroutines) 4 | [![Build Status](https://travis-ci.org/marusama/cyclicbarrier.svg?branch=master)](https://travis-ci.org/marusama/cyclicbarrier) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/marusama/cyclicbarrier)](https://goreportcard.com/report/github.com/marusama/cyclicbarrier) 6 | [![Coverage Status](https://coveralls.io/repos/github/marusama/cyclicbarrier/badge.svg?branch=master)](https://coveralls.io/github/marusama/cyclicbarrier?branch=master) 7 | [![GoDoc](https://godoc.org/github.com/marusama/cyclicbarrier?status.svg)](https://godoc.org/github.com/marusama/cyclicbarrier) 8 | [![License](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](LICENSE) 9 | 10 | CyclicBarrier is a synchronizer that allows a set of goroutines to wait for each other to reach a common execution point, also called a barrier. 11 | 12 | Inspired by Java CyclicBarrier https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/CyclicBarrier.html and C# Barrier https://msdn.microsoft.com/en-us/library/system.threading.barrier(v=vs.110).aspx 13 | 14 | ### Usage 15 | Initiate 16 | ```go 17 | import "github.com/marusama/cyclicbarrier" 18 | ... 19 | b1 := cyclicbarrier.New(10) // new cyclic barrier with parties = 10 20 | ... 21 | b2 := cyclicbarrier.NewWithAction(10, func() error { return nil }) // new cyclic barrier with parties = 10 and with defined barrier action 22 | ``` 23 | Await 24 | ```go 25 | b.Await(ctx) // await other parties 26 | ``` 27 | Reset 28 | ```go 29 | b.Reset() // reset the barrier 30 | ``` 31 | 32 | ### Simple example 33 | ```go 34 | // create a barrier for 10 parties with an action that increments counter 35 | // this action will be called each time when all goroutines reach the barrier 36 | cnt := 0 37 | b := cyclicbarrier.NewWithAction(10, func() error { 38 | cnt++ 39 | return nil 40 | }) 41 | 42 | wg := sync.WaitGroup{} 43 | for i := 0; i < 10; i++ { // create 10 goroutines (the same count as barrier parties) 44 | wg.Add(1) 45 | go func() { 46 | for j := 0; j < 5; j++ { 47 | 48 | // do some hard work 5 times 49 | time.Sleep(100 * time.Millisecond) 50 | 51 | err := b.Await(context.TODO()) // ..and wait for other parties on the barrier. 52 | // Last arrived goroutine will do the barrier action 53 | // and then pass all other goroutines to the next round 54 | if err != nil { 55 | panic(err) 56 | } 57 | } 58 | wg.Done() 59 | }() 60 | } 61 | 62 | wg.Wait() 63 | fmt.Println(cnt) // cnt = 5, it means that the barrier was passed 5 times 64 | ``` 65 | 66 | For more documentation see https://godoc.org/github.com/marusama/cyclicbarrier 67 | -------------------------------------------------------------------------------- /cyclicbarrier.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Maru Sama. All rights reserved. 2 | // Use of this source code is governed by the MIT license 3 | // that can be found in the LICENSE file. 4 | 5 | // Package cyclicbarrier provides an implementation of Cyclic Barrier primitive. 6 | package cyclicbarrier // import "github.com/marusama/cyclicbarrier" 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "sync" 12 | ) 13 | 14 | // CyclicBarrier is a synchronizer that allows a set of goroutines to wait for each other 15 | // to reach a common execution point, also called a barrier. 16 | // CyclicBarriers are useful in programs involving a fixed sized party of goroutines 17 | // that must occasionally wait for each other. 18 | // The barrier is called cyclic because it can be re-used after the waiting goroutines are released. 19 | // A CyclicBarrier supports an optional Runnable command that is run once per barrier point, 20 | // after the last goroutine in the party arrives, but before any goroutines are released. 21 | // This barrier action is useful for updating shared-state before any of the parties continue. 22 | type CyclicBarrier interface { 23 | // Await waits until all parties have invoked await on this barrier. 24 | // If the barrier is reset while any goroutine is waiting, or if the barrier is broken when await is invoked, 25 | // or while any goroutine is waiting, then ErrBrokenBarrier is returned. 26 | // If any goroutine is interrupted by ctx.Done() while waiting, then all other waiting goroutines 27 | // will return ErrBrokenBarrier and the barrier is placed in the broken state. 28 | // If the current goroutine is the last goroutine to arrive, and a non-nil barrier action was supplied in the constructor, 29 | // then the current goroutine runs the action before allowing the other goroutines to continue. 30 | // If an error occurs during the barrier action then that error will be returned and the barrier is placed in the broken state. 31 | Await(ctx context.Context) error 32 | 33 | // Reset resets the barrier to its initial state. 34 | // If any parties are currently waiting at the barrier, they will return with a ErrBrokenBarrier. 35 | Reset() 36 | 37 | // GetNumberWaiting returns the number of parties currently waiting at the barrier. 38 | GetNumberWaiting() int 39 | 40 | // GetParties returns the number of parties required to trip this barrier. 41 | GetParties() int 42 | 43 | // IsBroken queries if this barrier is in a broken state. 44 | // Returns true if one or more parties broke out of this barrier due to interruption by ctx.Done() or the last reset, 45 | // or a barrier action failed due to an error; false otherwise. 46 | IsBroken() bool 47 | } 48 | 49 | var ( 50 | // ErrBrokenBarrier error used when a goroutine tries to wait upon a barrier that is in a broken state, 51 | // or which enters the broken state while the goroutine is waiting. 52 | ErrBrokenBarrier = errors.New("broken barrier") 53 | ) 54 | 55 | // round 56 | type round struct { 57 | count int // count of goroutines for this roundtrip 58 | waitCh chan struct{} // wait channel for this roundtrip 59 | brokeCh chan struct{} // channel for isBroken broadcast 60 | isBroken bool // is barrier broken 61 | } 62 | 63 | // cyclicBarrier impl CyclicBarrier intf 64 | type cyclicBarrier struct { 65 | parties int 66 | barrierAction func() error 67 | 68 | lock sync.RWMutex 69 | round *round 70 | } 71 | 72 | // New initializes a new instance of the CyclicBarrier, specifying the number of parties. 73 | func New(parties int) CyclicBarrier { 74 | if parties <= 0 { 75 | panic("parties must be positive number") 76 | } 77 | return &cyclicBarrier{ 78 | parties: parties, 79 | lock: sync.RWMutex{}, 80 | round: &round{ 81 | waitCh: make(chan struct{}), 82 | brokeCh: make(chan struct{}), 83 | }, 84 | } 85 | } 86 | 87 | // NewWithAction initializes a new instance of the CyclicBarrier, 88 | // specifying the number of parties and the barrier action. 89 | func NewWithAction(parties int, barrierAction func() error) CyclicBarrier { 90 | if parties <= 0 { 91 | panic("parties must be positive number") 92 | } 93 | return &cyclicBarrier{ 94 | parties: parties, 95 | lock: sync.RWMutex{}, 96 | round: &round{ 97 | waitCh: make(chan struct{}), 98 | brokeCh: make(chan struct{}), 99 | }, 100 | barrierAction: barrierAction, 101 | } 102 | } 103 | 104 | func (b *cyclicBarrier) Await(ctx context.Context) error { 105 | var ( 106 | ctxDoneCh <-chan struct{} 107 | ) 108 | if ctx != nil { 109 | ctxDoneCh = ctx.Done() 110 | } 111 | 112 | // check if context is done 113 | select { 114 | case <-ctxDoneCh: 115 | return ctx.Err() 116 | default: 117 | } 118 | 119 | b.lock.Lock() 120 | 121 | // check if broken 122 | if b.round.isBroken { 123 | b.lock.Unlock() 124 | return ErrBrokenBarrier 125 | } 126 | 127 | // increment count of waiters 128 | b.round.count++ 129 | 130 | // saving in local variables to prevent race 131 | waitCh := b.round.waitCh 132 | brokeCh := b.round.brokeCh 133 | count := b.round.count 134 | 135 | b.lock.Unlock() 136 | 137 | if count > b.parties { 138 | panic("CyclicBarrier.Await is called more than count of parties") 139 | } 140 | 141 | if count < b.parties { 142 | // wait other parties 143 | select { 144 | case <-waitCh: 145 | return nil 146 | case <-brokeCh: 147 | return ErrBrokenBarrier 148 | case <-ctxDoneCh: 149 | b.breakBarrier(true) 150 | return ctx.Err() 151 | } 152 | } else { 153 | // we are last, run the barrier action and reset the barrier 154 | if b.barrierAction != nil { 155 | err := b.barrierAction() 156 | if err != nil { 157 | b.breakBarrier(true) 158 | return err 159 | } 160 | } 161 | b.reset(true) 162 | return nil 163 | } 164 | } 165 | 166 | func (b *cyclicBarrier) breakBarrier(needLock bool) { 167 | if needLock { 168 | b.lock.Lock() 169 | defer b.lock.Unlock() 170 | } 171 | 172 | if !b.round.isBroken { 173 | b.round.isBroken = true 174 | 175 | // broadcast 176 | close(b.round.brokeCh) 177 | } 178 | } 179 | 180 | func (b *cyclicBarrier) reset(safe bool) { 181 | b.lock.Lock() 182 | defer b.lock.Unlock() 183 | 184 | if safe { 185 | // broadcast to pass waiting goroutines 186 | close(b.round.waitCh) 187 | 188 | } else if b.round.count > 0 { 189 | b.breakBarrier(false) 190 | } 191 | 192 | // create new round 193 | b.round = &round{ 194 | waitCh: make(chan struct{}), 195 | brokeCh: make(chan struct{}), 196 | } 197 | } 198 | 199 | func (b *cyclicBarrier) Reset() { 200 | b.reset(false) 201 | } 202 | 203 | func (b *cyclicBarrier) GetParties() int { 204 | return b.parties 205 | } 206 | 207 | func (b *cyclicBarrier) GetNumberWaiting() int { 208 | b.lock.RLock() 209 | defer b.lock.RUnlock() 210 | 211 | return b.round.count 212 | } 213 | 214 | func (b *cyclicBarrier) IsBroken() bool { 215 | b.lock.RLock() 216 | defer b.lock.RUnlock() 217 | 218 | return b.round.isBroken 219 | } 220 | -------------------------------------------------------------------------------- /cyclicbarrier_test.go: -------------------------------------------------------------------------------- 1 | package cyclicbarrier 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "sync" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func checkBarrier(t *testing.T, b CyclicBarrier, 13 | expectedParties, expectedNumberWaiting int, expectedIsBroken bool) { 14 | 15 | parties, numberWaiting := b.GetParties(), b.GetNumberWaiting() 16 | isBroken := b.IsBroken() 17 | 18 | if expectedParties >= 0 && parties != expectedParties { 19 | t.Error("barrier must have parties = ", expectedParties, ", but has ", parties) 20 | } 21 | if expectedNumberWaiting >= 0 && numberWaiting != expectedNumberWaiting { 22 | t.Error("barrier must have numberWaiting = ", expectedNumberWaiting, ", but has ", numberWaiting) 23 | } 24 | if isBroken != expectedIsBroken { 25 | t.Error("barrier must have isBroken = ", expectedIsBroken, ", but has ", isBroken) 26 | } 27 | } 28 | 29 | func TestNew(t *testing.T) { 30 | tests := []func(){ 31 | func() { 32 | b := New(10) 33 | checkBarrier(t, b, 10, 0, false) 34 | if b.(*cyclicBarrier).barrierAction != nil { 35 | t.Error("barrier have unexpected barrierAction") 36 | } 37 | }, 38 | func() { 39 | defer func() { 40 | if recover() == nil { 41 | t.Error("Panic expected") 42 | } 43 | }() 44 | _ = New(0) 45 | }, 46 | func() { 47 | defer func() { 48 | if recover() == nil { 49 | t.Error("Panic expected") 50 | } 51 | }() 52 | _ = New(-1) 53 | }, 54 | } 55 | for _, test := range tests { 56 | test() 57 | } 58 | } 59 | 60 | func TestNewWithAction(t *testing.T) { 61 | tests := []func(){ 62 | func() { 63 | b := NewWithAction(10, func() error { return nil }) 64 | checkBarrier(t, b, 10, 0, false) 65 | if b.(*cyclicBarrier).barrierAction == nil { 66 | t.Error("barrier doesn't have expected barrierAction") 67 | } 68 | }, 69 | func() { 70 | b := NewWithAction(10, nil) 71 | checkBarrier(t, b, 10, 0, false) 72 | if b.(*cyclicBarrier).barrierAction != nil { 73 | t.Error("barrier have unexpected barrierAction") 74 | } 75 | }, 76 | func() { 77 | defer func() { 78 | if recover() == nil { 79 | t.Error("Panic expected") 80 | } 81 | }() 82 | _ = NewWithAction(0, func() error { return nil }) 83 | }, 84 | func() { 85 | defer func() { 86 | if recover() == nil { 87 | t.Error("Panic expected") 88 | } 89 | }() 90 | _ = NewWithAction(-1, func() error { return nil }) 91 | }, 92 | } 93 | for _, test := range tests { 94 | test() 95 | } 96 | } 97 | 98 | func TestAwaitOnce(t *testing.T) { 99 | n := 100 // goroutines count 100 | b := New(n) 101 | ctx := context.Background() 102 | 103 | wg := sync.WaitGroup{} 104 | for i := 0; i < n; i++ { 105 | wg.Add(1) 106 | go func() { 107 | err := b.Await(ctx) 108 | if err != nil { 109 | panic(err) 110 | } 111 | wg.Done() 112 | }() 113 | } 114 | 115 | wg.Wait() 116 | checkBarrier(t, b, n, 0, false) 117 | } 118 | 119 | func TestAwaitMany(t *testing.T) { 120 | n := 100 // goroutines count 121 | m := 1000 // inner cycle count 122 | b := New(n) 123 | ctx := context.Background() 124 | 125 | wg := sync.WaitGroup{} 126 | for i := 0; i < n; i++ { 127 | wg.Add(1) 128 | go func(num int) { 129 | for j := 0; j < m; j++ { 130 | err := b.Await(ctx) 131 | if err != nil { 132 | panic(err) 133 | } 134 | } 135 | wg.Done() 136 | }(i) 137 | } 138 | 139 | wg.Wait() 140 | checkBarrier(t, b, n, 0, false) 141 | } 142 | 143 | func TestAwaitOnceCtxDone(t *testing.T) { 144 | n := 100 // goroutines count 145 | b := New(n + 1) // parties are more than goroutines count so all goroutines will wait 146 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 147 | defer cancel() 148 | var deadlineCount, brokenBarrierCount int32 149 | 150 | wg := sync.WaitGroup{} 151 | for i := 0; i < n; i++ { 152 | wg.Add(1) 153 | go func(num int) { 154 | err := b.Await(ctx) 155 | if err == context.DeadlineExceeded { 156 | atomic.AddInt32(&deadlineCount, 1) 157 | } else if err == ErrBrokenBarrier { 158 | atomic.AddInt32(&brokenBarrierCount, 1) 159 | } else { 160 | panic("must be context.DeadlineExceeded or ErrBrokenBarrier error") 161 | } 162 | wg.Done() 163 | }(i) 164 | } 165 | 166 | wg.Wait() 167 | checkBarrier(t, b, n+1, -1, true) 168 | if deadlineCount == 0 { 169 | t.Error("must be more than 0 context.DeadlineExceeded errors, but found", deadlineCount) 170 | } 171 | if deadlineCount+brokenBarrierCount != int32(n) { 172 | t.Error("must be exactly", n, "context.DeadlineExceeded and ErrBrokenBarrier errors, but found", deadlineCount+brokenBarrierCount) 173 | } 174 | } 175 | 176 | func TestAwaitManyCtxDone(t *testing.T) { 177 | n := 100 // goroutines count 178 | b := New(n) 179 | ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) 180 | defer cancel() 181 | 182 | wg := sync.WaitGroup{} 183 | for i := 0; i < n; i++ { 184 | wg.Add(1) 185 | go func() { 186 | for { 187 | err := b.Await(ctx) 188 | if err != nil { 189 | if err != context.DeadlineExceeded && err != ErrBrokenBarrier { 190 | panic("must be context.DeadlineExceeded or ErrBrokenBarrier error") 191 | } 192 | break 193 | } 194 | } 195 | wg.Done() 196 | }() 197 | } 198 | 199 | wg.Wait() 200 | checkBarrier(t, b, n, -1, true) 201 | } 202 | 203 | func TestAwaitAction(t *testing.T) { 204 | n := 100 // goroutines count 205 | m := 1000 // inner cycle count 206 | ctx := context.Background() 207 | 208 | cnt := 0 209 | b := NewWithAction(n, func() error { 210 | cnt++ 211 | return nil 212 | }) 213 | 214 | wg := sync.WaitGroup{} 215 | for i := 0; i < n; i++ { 216 | wg.Add(1) 217 | go func() { 218 | for j := 0; j < m; j++ { 219 | err := b.Await(ctx) 220 | if err != nil { 221 | panic(err) 222 | } 223 | } 224 | wg.Done() 225 | }() 226 | } 227 | 228 | wg.Wait() 229 | checkBarrier(t, b, n, 0, false) 230 | if cnt != m { 231 | t.Error("cnt must be equal to = ", m, ", but it's ", cnt) 232 | } 233 | } 234 | 235 | func TestReset(t *testing.T) { 236 | n := 100 // goroutines count 237 | b := New(n + 1) // parties are more than goroutines count so all goroutines will wait 238 | ctx := context.Background() 239 | 240 | go func() { 241 | time.Sleep(30 * time.Millisecond) 242 | b.Reset() 243 | }() 244 | 245 | wg := sync.WaitGroup{} 246 | for i := 0; i < n; i++ { 247 | wg.Add(1) 248 | go func() { 249 | err := b.Await(ctx) 250 | if err != ErrBrokenBarrier { 251 | panic(err) 252 | } 253 | wg.Done() 254 | }() 255 | } 256 | 257 | wg.Wait() 258 | checkBarrier(t, b, n+1, 0, false) 259 | } 260 | 261 | func TestAwaitErrorInActionThenReset(t *testing.T) { 262 | n := 100 // goroutines count 263 | ctx := context.Background() 264 | 265 | errExpected := errors.New("test error") 266 | 267 | isActionCalled := false 268 | var expectedErrCount, errBrokenBarrierCount int32 269 | 270 | b := NewWithAction(n, func() error { 271 | isActionCalled = true 272 | return errExpected 273 | }) 274 | 275 | wg := sync.WaitGroup{} 276 | for i := 0; i < n; i++ { 277 | wg.Add(1) 278 | go func() { 279 | err := b.Await(ctx) 280 | if err == errExpected { 281 | atomic.AddInt32(&expectedErrCount, 1) 282 | } else if err == ErrBrokenBarrier { 283 | atomic.AddInt32(&errBrokenBarrierCount, 1) 284 | } else { 285 | panic(err) 286 | } 287 | wg.Done() 288 | }() 289 | } 290 | 291 | wg.Wait() 292 | checkBarrier(t, b, n, n, true) // check that barrier is broken 293 | if !isActionCalled { 294 | t.Error("barrier action must be called") 295 | } 296 | if !b.IsBroken() { 297 | t.Error("barrier must be broken via action error") 298 | } 299 | if expectedErrCount != 1 { 300 | t.Error("expectedErrCount must be equal to", 1, ", but it equals to", expectedErrCount) 301 | } 302 | if errBrokenBarrierCount != int32(n-1) { 303 | t.Error("expectedErrCount must be equal to", n-1, ", but it equals to", errBrokenBarrierCount) 304 | } 305 | 306 | // call await on broken barrier must return ErrBrokenBarrier 307 | if b.Await(ctx) != ErrBrokenBarrier { 308 | t.Error("call await on broken barrier must return ErrBrokenBarrier") 309 | } 310 | 311 | // do reset broken barrier 312 | b.Reset() 313 | if b.IsBroken() { 314 | t.Error("barrier must not be broken after reset") 315 | } 316 | checkBarrier(t, b, n, 0, false) 317 | } 318 | 319 | func TestAwaitTooMuchGoroutines(t *testing.T) { 320 | 321 | n := 100 // goroutines count 322 | m := 1000 // inner cycle count 323 | b := New(1) 324 | ctx := context.Background() 325 | 326 | var panicCount int32 327 | 328 | wg := sync.WaitGroup{} 329 | for i := 0; i < n; i++ { 330 | wg.Add(1) 331 | go func(num int) { 332 | defer func() { 333 | if recover() != nil { 334 | atomic.AddInt32(&panicCount, 1) 335 | } 336 | wg.Done() 337 | }() 338 | for j := 0; j < m; j++ { 339 | err := b.Await(ctx) 340 | if err != nil { 341 | panic(err) 342 | } 343 | } 344 | }(i) 345 | } 346 | 347 | wg.Wait() 348 | checkBarrier(t, b, 1, 0, false) 349 | 350 | if panicCount == 0 { 351 | t.Error("barrier must panic when await is called from too much goroutines") 352 | } 353 | } 354 | --------------------------------------------------------------------------------