Skip to content

Commit dd05964

Browse files
authored
Merge pull request #278 from markus-wa/issue-276/Parser-Cancel-deadlock
fix deadlock caused by multiple calls to Parser.Cancel()
2 parents 79b5028 + 117d3b9 commit dd05964

File tree

7 files changed

+143
-44
lines changed

7 files changed

+143
-44
lines changed

.golangci.yml

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,59 @@ run:
77
skip-files:
88
- parser_interface.go
99
- game_state_interface.go
10+
allow-parallel-runners: true
11+
1012
linters:
11-
enable-all: true
12-
disable:
13-
- gochecknoinits
14-
- gochecknoglobals
15-
- lll
16-
- typecheck
17-
- gomnd
18-
- exhaustivestruct
19-
- godot
20-
- gofumpt
21-
- testpackage
13+
disable-all: true
14+
enable:
15+
- bodyclose
16+
- deadcode
17+
- depguard
18+
- dogsled
19+
- dupl
20+
- exportloopref
21+
- exhaustive
22+
- funlen
23+
- goconst
24+
- gocritic
25+
- gocyclo
26+
- gofmt
27+
- goimports
28+
- golint
29+
- goprintffuncname
30+
- gosec
31+
- gosimple
32+
- govet
33+
- ineffassign
34+
- misspell
35+
- nakedret
36+
- noctx
37+
- nolintlint
38+
- rowserrcheck
39+
- staticcheck
40+
- structcheck
41+
- stylecheck
42+
- unconvert
43+
- unparam
44+
- unused
45+
- varcheck
46+
- whitespace
47+
- asciicheck
48+
- gocognit
49+
- godox
50+
- nestif
51+
- prealloc
52+
- revive
53+
- wsl
54+
2255
issues:
2356
exclude-rules:
2457
# Exclude some linters from running on tests files.
2558
- path: _test\.go
2659
linters:
2760
- wsl
2861
- funlen
62+
2963
linters-settings:
3064
gocritic:
3165
disabled-checks:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/llgcode/draw2d v0.0.0-20200930101115-bfaf5d914d1e
88
github.com/markus-wa/go-unassert v0.1.2
99
github.com/markus-wa/gobitread v0.2.2
10-
github.com/markus-wa/godispatch v1.3.0
10+
github.com/markus-wa/godispatch v1.4.1
1111
github.com/markus-wa/quickhull-go/v2 v2.1.0
1212
github.com/stretchr/testify v1.6.1
1313
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ github.com/markus-wa/godispatch v1.2.1 h1:Bj6blK4xJ/72pDi0rprVYluOGW9CV55FUDFPAp
4343
github.com/markus-wa/godispatch v1.2.1/go.mod h1:GNzV7xdnZ9+VBi0z+hma9oUQrJmtqRrqyAuGKTTTcKY=
4444
github.com/markus-wa/godispatch v1.3.0 h1:eHT5Xm8ZEwilj9b/Tu7gyMfmtSau+yC0qpM7NRR+CQk=
4545
github.com/markus-wa/godispatch v1.3.0/go.mod h1:GNzV7xdnZ9+VBi0z+hma9oUQrJmtqRrqyAuGKTTTcKY=
46+
github.com/markus-wa/godispatch v1.4.0 h1:cSRg6/jvLjpi2616rJAbgXMfl8gV+6RfEAUe+8qVfYE=
47+
github.com/markus-wa/godispatch v1.4.0/go.mod h1:oz4Bh/xA+T/tMR5jXShPQ3fhkL5ba6VsVg9hLZzpEbw=
48+
github.com/markus-wa/godispatch v1.4.1 h1:Cdff5x33ShuX3sDmUbYWejk7tOuoHErFYMhUc2h7sLc=
49+
github.com/markus-wa/godispatch v1.4.1/go.mod h1:tk8L0yzLO4oAcFwM2sABMge0HRDJMdE8E7xm4gK/+xM=
4650
github.com/markus-wa/quickhull-go/v2 v2.1.0 h1:DA2pzEzH0k5CEnlUsouRqNdD+jzNFb4DBhrX4Hpa5So=
4751
github.com/markus-wa/quickhull-go/v2 v2.1.0/go.mod h1:bOlBUpIzGSMMhHX0f9N8CQs0VZD4nnPeta0OocH7m4o=
4852
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

pkg/demoinfocs/demoinfocs_test.go

Lines changed: 71 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ var concurrentDemos = flag.Int("concurrentdemos", 2, "The `number` of current de
3939

4040
var update = flag.Bool("update", false, "update .golden files")
4141

42+
//nolint:cyclop
4243
func TestDemoInfoCs(t *testing.T) {
44+
t.Parallel()
45+
4346
if testing.Short() {
4447
t.Skip("skipping test due to -short flag")
4548
}
@@ -62,21 +65,21 @@ func TestDemoInfoCs(t *testing.T) {
6265
actual.WriteString(fmt.Sprintf("%#v\n", e))
6366
})
6467

65-
fmt.Println("Parsing header")
68+
t.Log("Parsing header")
6669
h, err := p.ParseHeader()
6770
assertions.NoError(err, "error returned by Parser.ParseHeader()")
6871
assertions.Equal(h, p.Header(), "values returned by ParseHeader() and Header() don't match")
69-
fmt.Printf("Header: %v - FrameRate()=%.2f frames/s ; FrameTime()=%s ; TickRate()=%.2f frames/s ; TickTime()=%s\n", h, h.FrameRate(), h.FrameTime(), p.TickRate(), p.TickTime())
72+
t.Logf("Header: %v - FrameRate()=%.2f frames/s ; FrameTime()=%s ; TickRate()=%.2f frames/s ; TickTime()=%s\n", h, h.FrameRate(), h.FrameTime(), p.TickRate(), p.TickTime())
7073

71-
fmt.Println("Registering handlers")
74+
t.Log("Registering handlers")
7275
gs := p.GameState()
7376
p.RegisterEventHandler(func(e events.RoundEnd) {
7477
var (
7578
winner, loser *common.TeamState
7679
winnerSide string
7780
)
7881

79-
switch e.Winner {
82+
switch e.Winner { //nolint:exhaustive
8083
case common.TeamTerrorists:
8184
winner = gs.TeamTerrorists()
8285
loser = gs.TeamCounterTerrorists()
@@ -87,7 +90,8 @@ func TestDemoInfoCs(t *testing.T) {
8790
winnerSide = "CT"
8891
default:
8992
// Probably match medic or something similar
90-
fmt.Println("Round finished: No winner (tie)")
93+
t.Log("Round finished: No winner (tie)")
94+
9195
return
9296
}
9397

@@ -100,7 +104,7 @@ func TestDemoInfoCs(t *testing.T) {
100104
currentFrame := p.CurrentFrame()
101105

102106
// Score + 1 for winner because it hasn't actually been updated yet
103-
fmt.Printf("Round finished: score=%d:%d ; winnerSide=%s ; clanName=%q ; teamId=%d ; teamFlag=%s ; ingameTime=%s ; progress=%.1f%% ; tick=%d ; frame=%d\n", winner.Score()+1, loser.Score(), winnerSide, winnerClan, winnerID, winnerFlag, ingameTime, progressPercent, ingameTick, currentFrame)
107+
t.Logf("Round finished: score=%d:%d ; winnerSide=%s ; clanName=%q ; teamId=%d ; teamFlag=%s ; ingameTime=%s ; progress=%.1f%% ; tick=%d ; frame=%d\n", winner.Score()+1, loser.Score(), winnerSide, winnerClan, winnerID, winnerFlag, ingameTime, progressPercent, ingameTick, currentFrame)
104108
if len(winnerClan) == 0 || winnerID == 0 || len(winnerFlag) == 0 || ingameTime == 0 || progressPercent == 0 || ingameTick == 0 || currentFrame == 0 {
105109
t.Error("Unexprected default value, check output of last round")
106110
}
@@ -165,31 +169,33 @@ func TestDemoInfoCs(t *testing.T) {
165169
// Net-message stuff
166170
var netTickHandlerID dispatch.HandlerIdentifier
167171
netTickHandlerID = p.RegisterNetMessageHandler(func(tick *msg.CNETMsg_Tick) {
168-
fmt.Println("Net-message tick handled, unregistering - tick:", tick.Tick)
172+
t.Log("Net-message tick handled, unregistering - tick:", tick.Tick)
169173
p.UnregisterNetMessageHandler(netTickHandlerID)
170174
})
171175

172176
ts := time.Now()
173177

174178
frameByFrameCount := 1000
175-
fmt.Printf("Parsing frame by frame (%d frames)\n", frameByFrameCount)
179+
t.Logf("Parsing frame by frame (%d frames)\n", frameByFrameCount)
176180

177181
for i := 0; i < frameByFrameCount; i++ {
178182
ok, err := p.ParseNextFrame()
179183
assertions.NoError(err, "error occurred in ParseNextFrame()")
180184
assertions.True(ok, "parser reported end of demo after less than %d frames", frameByFrameCount)
181185
}
182186

183-
fmt.Println("Parsing to end")
187+
t.Log("Parsing to end")
184188
err = p.ParseToEnd()
185189
assertions.NoError(err, "error occurred in ParseToEnd()")
186190

187-
fmt.Printf("Took %s\n", time.Since(ts))
191+
t.Logf("Took %s\n", time.Since(ts))
188192

189193
assertGolden(t, assertions, "default", actual.Bytes())
190194
}
191195

192196
func TestUnexpectedEndOfDemo(t *testing.T) {
197+
t.Parallel()
198+
193199
if testing.Short() {
194200
t.Skip("skipping test due to -short flag")
195201
}
@@ -203,7 +209,9 @@ func TestUnexpectedEndOfDemo(t *testing.T) {
203209
assert.Equal(t, demoinfocs.ErrUnexpectedEndOfDemo, err, "parsing cancelled but error was not ErrUnexpectedEndOfDemo")
204210
}
205211

206-
func TestCancelParseToEnd(t *testing.T) {
212+
func TestParseToEnd_Cancel(t *testing.T) {
213+
t.Parallel()
214+
207215
if testing.Short() {
208216
t.Skip("skipping test")
209217
}
@@ -232,7 +240,36 @@ func TestCancelParseToEnd(t *testing.T) {
232240
assert.True(t, tix == maxTicks, "FrameDone handler was not triggered the correct amount of times")
233241
}
234242

243+
// See https://github.com/markus-wa/demoinfocs-golang/issues/276
244+
func TestParseToEnd_MultiCancel(t *testing.T) {
245+
t.Parallel()
246+
247+
if testing.Short() {
248+
t.Skip("skipping test")
249+
}
250+
251+
f := openFile(t, defaultDemPath)
252+
defer mustClose(t, f)
253+
254+
p := demoinfocs.NewParser(f)
255+
256+
var handlerID dispatch.HandlerIdentifier
257+
handlerID = p.RegisterEventHandler(func(events.FrameDone) {
258+
p.Cancel()
259+
p.Cancel()
260+
p.Cancel()
261+
p.Cancel()
262+
p.Cancel()
263+
p.UnregisterEventHandler(handlerID)
264+
})
265+
266+
err := p.ParseToEnd()
267+
assert.Equal(t, demoinfocs.ErrCancelled, err, "parsing cancelled but error was not ErrCancelled")
268+
}
269+
235270
func TestInvalidFileType(t *testing.T) {
271+
t.Parallel()
272+
236273
invalidDemoData := make([]byte, 2048)
237274
_, err := rand.Read(invalidDemoData)
238275
assert.NoError(t, err, "failed to create/read random data")
@@ -253,6 +290,8 @@ func TestInvalidFileType(t *testing.T) {
253290
}
254291

255292
func TestConcurrent(t *testing.T) {
293+
t.Parallel()
294+
256295
if testing.Short() {
257296
t.Skip("skipping test")
258297
}
@@ -262,19 +301,21 @@ func TestConcurrent(t *testing.T) {
262301
var i int64
263302
runner := func() {
264303
n := atomic.AddInt64(&i, 1)
265-
fmt.Printf("Starting concurrent runner %d\n", n)
304+
t.Logf("Starting concurrent runner %d\n", n)
266305

267306
ts := time.Now()
268307

269308
parseDefaultDemo(t)
270309

271-
fmt.Printf("Runner %d took %s\n", n, time.Since(ts))
310+
t.Logf("Runner %d took %s\n", n, time.Since(ts))
272311
}
273312

274313
runConcurrently(runner)
275314
}
276315

277316
func parseDefaultDemo(tb testing.TB) {
317+
tb.Helper()
318+
278319
f := openFile(tb, defaultDemPath)
279320
defer mustClose(tb, f)
280321

@@ -297,6 +338,8 @@ func runConcurrently(runner func()) {
297338
}
298339

299340
func TestDemoSet(t *testing.T) {
341+
t.Parallel()
342+
300343
if testing.Short() {
301344
t.Skip("skipping test due to -short flag")
302345
}
@@ -307,7 +350,7 @@ func TestDemoSet(t *testing.T) {
307350
for _, d := range dems {
308351
name := d.Name()
309352
if strings.HasSuffix(name, ".dem") {
310-
fmt.Printf("Parsing '%s/%s'\n", demSetPath, name)
353+
t.Logf("Parsing '%s/%s'\n", demSetPath, name)
311354
func() {
312355
f := openFile(t, fmt.Sprintf("%s/%s", demSetPath, name))
313356
defer mustClose(t, f)
@@ -318,6 +361,12 @@ func TestDemoSet(t *testing.T) {
318361

319362
p := demoinfocs.NewParser(f)
320363

364+
p.RegisterEventHandler(func(event events.GenericGameEvent) {
365+
if event.Name == "bot_takeover" {
366+
t.Log(event.Data)
367+
}
368+
})
369+
321370
err = p.ParseToEnd()
322371
assert.Nil(t, err, "parsing of '%s/%s' failed", demSetPath, name)
323372
}()
@@ -362,13 +411,17 @@ func BenchmarkConcurrent(b *testing.B) {
362411
}
363412

364413
func openFile(tb testing.TB, file string) *os.File {
414+
tb.Helper()
415+
365416
f, err := os.Open(file)
366417
assert.NoError(tb, err, "failed to open file %q", file)
367418

368419
return f
369420
}
370421

371422
func assertGolden(tb testing.TB, assertions *assert.Assertions, testCase string, actual []byte) {
423+
tb.Helper()
424+
372425
const goldenVerificationGoVersionMin = "go1.12"
373426
if ver := runtime.Version(); strings.Compare(ver, goldenVerificationGoVersionMin) < 0 {
374427
tb.Logf("old go version %q detected, skipping golden file verification", ver)
@@ -417,15 +470,18 @@ func assertGolden(tb testing.TB, assertions *assert.Assertions, testCase string,
417470

418471
func removePointers(s []byte) []byte {
419472
r := regexp.MustCompile(`\(0x[\da-f]{10}\)`)
473+
420474
return r.ReplaceAll(s, []byte("(non-nil)"))
421475
}
422476

423477
func writeFile(assertions *assert.Assertions, file string, data []byte) {
424-
err := ioutil.WriteFile(file, data, 0755)
478+
err := ioutil.WriteFile(file, data, 0600)
425479
assertions.NoError(err, "failed to write to file %q", file)
426480
}
427481

428482
func mustClose(tb testing.TB, closables ...io.Closer) {
483+
tb.Helper()
484+
429485
mustCloseAssert(assert.New(tb), closables...)
430486
}
431487

pkg/demoinfocs/parser.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ type parser struct {
5959
header *common.DemoHeader // Pointer so we can check for nil
6060
gameState *gameState
6161
demoInfoProvider demoInfoProvider // Provides demo infos to other packages that the core package depends on
62-
cancelChan chan struct{} // Non-anime-related, used for aborting the parsing
6362
err error // Contains a error that occurred during parsing if any
6463
errLock sync.Mutex // Used to sync up error mutations between parsing & handling go-routines
6564

@@ -250,12 +249,20 @@ func (p *parser) error() error {
250249
}
251250

252251
func (p *parser) setError(err error) {
253-
if err == nil || p.err != nil {
252+
if err == nil {
254253
return
255254
}
256255

257256
p.errLock.Lock()
257+
258+
if p.err != nil {
259+
p.errLock.Unlock()
260+
261+
return
262+
}
263+
258264
p.err = err
265+
259266
p.errLock.Unlock()
260267
}
261268

@@ -303,7 +310,6 @@ func NewParserWithConfig(demostream io.Reader, config ParserConfig) Parser {
303310
p.equipmentMapping = make(map[*st.ServerClass]common.EquipmentType)
304311
p.rawPlayers = make(map[int]*playerInfo)
305312
p.triggers = make(map[int]*boundingBoxInformation)
306-
p.cancelChan = make(chan struct{}, 1)
307313
p.demoInfoProvider = demoInfoProvider{parser: &p}
308314
p.gameState = newGameState(p.demoInfoProvider)
309315
p.grenadeModelIndices = make(map[int]common.EquipmentType)

pkg/demoinfocs/parser_interface.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ type Parser interface {
112112
//
113113
// See also: ParseNextFrame() for other possible errors.
114114
ParseToEnd() (err error)
115-
// Cancel aborts ParseToEnd().
116-
// All information that was already read up to this point may still be used (and new events may still be sent out).
115+
// Cancel aborts ParseToEnd() and drains the internal event queues.
116+
// No further events will be sent to event or message handlers after this.
117117
Cancel()
118118
/*
119119
ParseNextFrame attempts to parse the next frame / demo-tick (not ingame tick).

0 commit comments

Comments
 (0)