// Parse takes a string and begins the delegation to potential processors. To // avoid deadlocks, inconsistencies, race conditions and other unmentionables // we lock the location of the player. However there is a race condition // between getting the player's location and locking it - they may have moved // in-between. We therefore get and lock their current location then check it's // still their current location. If it is not we unlock and try again. // // If a command effects more than one location we have to release the current // lock on the location and relock the locations in Unique Id order before // trying again. Always locking in a consistent order greatly helps in avoiding // deadlocks. // // MOST of the time we are only interested in a few things: The current player, // it's location, items at the location, mobiles at the location. We can // therefore avoid complex fine grained locking on each individual Thing and // just lock on the whole location. This does mean if there are a LOT of things // happening in one specific location we will not have as much parallelism as we // would like. // // TODO: If there many clients trying to connect at once - say 250+ simultaneous // clients connecting - then the starting location becomes a bit of a bottle // neck (at 1,000+ simultaneous clients connecting is a pain - but once // connected things smooth out and become playable again). Adding more starting // locations help to spread the bottle neck. Note that this is just an issue // with the initial connection and multiple clients all trying to grab the start // location lock! func (p *Player) Parse(input string) { // If no input respond with nothing so the prompt is redisplayed if input == "" { p.Respond("") return } cmd := command.New(p, input) cmd.AddLock(p.Locate()) cmd.LocksModified() // Another funky looking for loop :) for p.parseStage2(cmd) { } }
// add places a player in the world safely and announces their arrival. We // manually build and parse the 'LOOK' command to avoid deadlocking - adding // the player locks the location as does a normal p.Parse('LOOK'). We could add // the player and then parse but that would require obtaining the lock twice. func (p *Player) add(l location.Interface) { l.Lock() defer l.Unlock() l.Add(p) PlayerList.Add(p) cmd := command.New(p, "LOOK") p.Process(cmd) if !l.Crowded() { cmd.Broadcast([]thing.Interface{p}, "There is a puff of smoke and %s appears spluttering and coughing.", p.Name()) } cmd.Flush() }
func TestProcess(t *testing.T) { // Setup 'will' process will := &willProcess{&thing.Thing{}} will.Thing.Unmarshal(recordjar.Record{ "name": "Harness 1", "aliases": "HARNESS1", ":data:": "This is test harness 1.", }) // Setup 'wont' process wont := &wontProcess{&thing.Thing{}} wont.Thing.Unmarshal(recordjar.Record{ "name": "Harness 2", "aliases": "HARNESS2", ":data:": "This is test harness 2.", }) // Test with 'will' which can process commands inv := Inventory{} inv.Add(will) // Check recursion. 'will' should not be delegated to when also issuing command { have := inv.Process(command.New(will, "TEST")) want := false if have != want { t.Errorf("Process mis-handled: have %t wanted %t", have, want) } } // 'will' should handle command from 'wont' { have := inv.Process(command.New(wont, "TEST")) want := true if have != want { t.Errorf("Process not handled: have %t wanted %t", have, want) } } // Test with 'wont' which cannot process commands inv.Remove(will) inv.Add(wont) // 'wont' cannot handle command from 'will' { have := inv.Process(command.New(will, "TEST")) want := false if have != want { t.Errorf("Process mis-handled: have %t wanted %t", have, want) } } // 'wont' cannot handle command from self { have := inv.Process(command.New(wont, "TEST")) want := false if have != want { t.Errorf("Process mis-handled: have %t wanted %t", have, want) } } }