/// <summary>Get the node that owns the specified hash code</summary> public CacheNode GetNodeForHash(int hashCode) { this.ringLock.EnterReadLock(); try { if (this.SortedLocations == null || this.SortedLocations.Count == 0) { throw new Exception("SortedLocations are null or empty"); } // ---- n1 ----- n2 ----- n3 ----- n4 ----- n5 // --------- h ------------------------------- // 100 200 300 400 500 600 700 800 900 1000 // For now simply iterate over the list until we find the bucket. // On the ring, the node to clockwise owns the angle between it and // the last node. Above, n2 owns from 201 to 400, and therefore h. // We can surely speed this up by dividing and conquering. CacheNode firstNode = null; foreach (var kvp in this.SortedLocations) { if (firstNode == null) { firstNode = kvp.Value; } if (kvp.Key >= hashCode) { return(kvp.Value); } } // We wrapped around Int.MAX return(firstNode); } finally { this.ringLock.ExitReadLock(); } }
/// <summary>Parse a line of the config file with a Node configuration</summary> public static CacheNode ParseNodeLine(string line) { string[] tokens = SplitLine(line); if (tokens.Length != 3) { throw new Exception("Invalid configuration line: " + line); } // # Node host:port MaxMem // Node localhost:12346 24Mb string hostPortStr = tokens[1]; string maxMemStr = tokens[2]; CacheNode node = new CacheNode(); node.MaxNumBytes = ParseMaxNumBytes(maxMemStr); string[] hostPortTokens = hostPortStr.Split(':'); if (hostPortTokens.Length != 2) { throw new Exception( "Invalid configuration line (host:port): " + line); } node.HostName = hostPortTokens[0]; int portNumber; if (!int.TryParse(hostPortTokens[1], out portNumber)) { throw new Exception( "Invalid configuration line (port): " + line); } node.PortNumber = portNumber; return(node); }
/// <summary>Adds a node to the ring</summary> /// <remarks>This is only called on the master. Data nodes will /// simply reload everything when something changes</remarks> public void AddNode(CacheNode node) { this.ringLock.EnterWriteLock(); try { // Make sure this node isn't already in the ring string nodeName = node.GetName(); if (this.Nodes.ContainsKey(nodeName)) { throw new Exception("Already added node " + nodeName); } this.Nodes.Add(nodeName, node); DetermineNodeLocations(); LookupEndPoints(); } finally { this.ringLock.ExitWriteLock(); } }
/// <summary> /// Parse the config file. /// </summary> public static CacheConfig Load(string fileName) { CacheConfig config = new CacheConfig(); CacheRing ring = new CacheRing(); config.Ring = ring; // Only gets populated for the master node int lineNum = 0; using (StreamReader sr = new StreamReader(fileName)) { string line = null; while (true) { line = sr.ReadLine(); if (line == null) { break; } lineNum++; ConfigLine configLine = new ConfigLine(); configLine.Line = line; config.ConfigLines.Add(lineNum, configLine); line = line.Trim().ToLower(); if (line.Equals(String.Empty)) { continue; } if (line.StartsWith("#")) { continue; } string[] tokens = SplitLine(line); if (line.StartsWith("node")) { CacheNode node = ParseNodeLine(line); node.Status = CacheNodeStatus.Up; ring.AddNode(node); configLine.IsNodeLine = true; } else if (line.StartsWith("listener")) { // # Listener host ip:port IsMaster Yes|No // Listener localhost 127.0.0.1:12345 Yes if (tokens.Length != 4) { throw new Exception("Invalid config line: " + line); } config.ListenerHostName = tokens[1]; string ipPort = tokens[2]; string[] ipPortTokens = ipPort.Split(':'); if (ipPortTokens.Length != 2) { throw new Exception("Invalid config line: " + line); } config.ListenerIP = ipPortTokens[0]; int lpn; if (!int.TryParse(ipPortTokens[1], out lpn)) { throw new Exception("Invalid config line: " + line); } config.ListenerPortNumber = lpn; if (tokens[3].Equals("yes")) { config.IsMaster = true; } else if (tokens[3].Equals("no")) { config.IsMaster = false; } else { throw new Exception( "Invalid config line, IsMaster should be Yes or No: " + line); } } else if (line.StartsWith("trace")) { // # Trace On|Off File // Trace On C:\Loop\Logs\LoopCacheMaster.txt if (tokens.Length != 3) { throw new Exception("Invalid config line: " + line); } if (tokens[1].Equals("on")) { config.IsTraceEnabled = true; } else if (tokens[1].Equals("off")) { config.IsTraceEnabled = false; } else { throw new Exception( "Invalid config line, Trace should be On of Off: " + line); } config.TraceFilePath = tokens[2]; } else if (line.StartsWith("master")) { // # Master host:port // Master localhost:12345 if (tokens.Length != 2) { throw new Exception(string.Format( "Invalid config line, {0} tokens: {1}", tokens.Length, line)); } string[] hostPortTokens = tokens[1].Split(':'); if (hostPortTokens.Length != 2) { throw new Exception("Invalid config line, host:port: " + line); } config.MasterHostName = hostPortTokens[0]; int mpn; if (!int.TryParse(hostPortTokens[1], out mpn)) { throw new Exception("Invalid config line, port number: " + line); } config.MasterPortNumber = mpn; } } } return(config); }
/// <summary>Test basic ring functionality.</summary> /// <remarks>Throws an exception or returns false on failure</remarks> public static bool Test() { CacheRing ring = new CacheRing(); CacheNode nodeA = new CacheNode(); nodeA.HostName = "localhost"; nodeA.PortNumber = 1; nodeA.MaxNumBytes = CacheConfig.ParseMaxNumBytes("48Mb"); ring.Nodes.Add(nodeA.GetName(), nodeA); CacheNode nodeB = new CacheNode(); nodeB.HostName = "localhost"; nodeB.PortNumber = 2; nodeB.MaxNumBytes = CacheConfig.ParseMaxNumBytes("12Mb"); ring.Nodes.Add(nodeB.GetName(), nodeB); CacheNode nodeC = new CacheNode(); nodeC.HostName = "localhost"; nodeC.PortNumber = 3; nodeC.MaxNumBytes = CacheConfig.ParseMaxNumBytes("64Mb"); ring.Nodes.Add(nodeC.GetName(), nodeC); // Hard-code some locations so we can make sure objects get assigned // to the correct virtual node. ring.SortedLocations.Add(10, nodeA); nodeA.Locations.Add(10); ring.SortedLocations.Add(-10, nodeB); nodeB.Locations.Add(-10); ring.SortedLocations.Add(20, nodeC); nodeC.Locations.Add(20); ring.SortedLocations.Add(50, nodeA); nodeA.Locations.Add(50); ring.SortedLocations.Add(60, nodeC); nodeC.Locations.Add(60); var node = ring.GetNodeForHash(5); if (node != nodeA) { throw new Exception(string.Format ("Hash 5 should belong to nodeA, not {0}", node.GetName())); } node = ring.GetNodeForHash(int.MaxValue); if (node != nodeB) { throw new Exception(string.Format ("Hash Integer.MAX should belong to nodeB, not {0}", node.GetName())); } node = ring.GetNodeForHash(20); if (node != nodeC) { throw new Exception(string.Format ("Hash 20 should belong to nodeC, not {0}", node.GetName())); } node = ring.GetNodeForHash(25); if (node != nodeA) { throw new Exception(string.Format ("Hash 25 should belong to nodeA, not {0}", node.GetName())); } // Now get rid of those hard coded locations and let the algorithm decide ring.DetermineNodeLocations(); // Make sure the master list and the nodes agree foreach (var n in ring.Nodes.Values) { foreach (var location in n.Locations) { if (!ring.SortedLocations.ContainsKey(location) || ring.SortedLocations[location] != n) { throw new Exception(string.Format ("Location {0} in node {1} not found", location, node.GetName())); } } } foreach (var loc in ring.SortedLocations) { string nodeName = loc.Value.GetName(); if (!ring.Nodes.ContainsKey(nodeName)) { throw new Exception(string.Format ("ring.Nodes missing {0}", nodeName)); } if (!loc.Value.Locations.Contains(loc.Key)) { throw new Exception(string.Format ("node {0} does not have {1}", nodeName, loc.Key)); } } //Console.WriteLine(ring.GetTrace()); // Now let's place a bunch of values and see how many of them change // when we make changes to the node configuration. //Console.WriteLine("About to place objects"); // nodeName => List of hashes Dictionary <string, List <int> > map = new Dictionary <string, List <int> >(); for (int i = 0; i < 100000; i++) { Guid g = Guid.NewGuid(); int hash = CacheHelper.GetConsistentHashCode(g.ToString()); CacheNode n = ring.GetNodeForHash(hash); string nodeName = n.GetName(); List <int> hashes; if (!map.TryGetValue(nodeName, out hashes)) { hashes = new List <int>(); map[nodeName] = hashes; } hashes.Add(hash); } //foreach (var nodeName in map.Keys) //{ // Console.WriteLine("{0} has {1} hashes", // nodeName, map[nodeName].Count); //} //Console.WriteLine("Modifying sizes and replacing objects"); nodeC.MaxNumBytes = CacheConfig.ParseMaxNumBytes("48Mb"); // was 64Mb ring.PopulateNodes(); int numChanged = 0; int numTotal = 0; foreach (var nodeName in map.Keys) { foreach (int hash in map[nodeName]) { numTotal++; CacheNode n = ring.GetNodeForHash(hash); if (!(nodeName.Equals(n.GetName()))) { numChanged++; } } } if (numChanged >= numTotal) { throw new Exception("Number of changed hases >= total number of hashes"); } // TODO - Caclulate an acceptable percentage //Console.WriteLine("{0} hashes changed which node they were assigned to", // numChanged); return(true); }