libdb/examples/csharp/excs_repquote/RepQuoteExample.cs

710 lines
21 KiB
C#
Raw Normal View History

2011-09-13 17:44:24 +00:00
/*-
* See the file LICENSE for redistribution information.
*
* Copyright (c) 2009, 2011 Oracle and/or its affiliates. All rights reserved.
*
*/
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using BerkeleyDB;
/**
* RepQuoteExample is a simple but complete demonstration of a replicated
* application. The application is a mock stock ticker. The master accepts a
* stock symbol and an numerical value as input, and stores this information
* into a replicated database; either master or clients can display the
* contents of the database.
*
* The options to start a given replication node are:
*
* -h home (required; h stands for home directory)
* -l host:port (required unless -L is specified; l stands for local)
* -L host:port (optional, L means group creator)
* -C or M (optional; start up as client or master)
* -r host:port (optional; r stands for remote; any number of these may
* be specified)
* -R host:port (optional; R stands for remote peer; only one of these may
* be specified)
* -a all|quorum (optional; a stands for ack policy)
* -b (optional; b stands for bulk)
* -p priority (optional; defaults to 100)
* -v (optional; v stands for verbose)
*
* A typical session begins with a command such as the following to start a
* master:
*
* excs_repquote.exe -M -h dir1 -l localhost:6000
*
* and several clients:
*
* excs_repquote.exe -C -h dir2
* -l localhost:6001 -r localhost:6000
* excs_repquote.exe -C -h dir3
* -l localhost:6002 -r localhost:6000
* excs_repquote.exe -C -h dir4
* -l localhost:6003 -r localhost:6000
*
* Each process is a member of a DB replication group. The sample application
* expects the following commands to stdin:
*
* NEWLINE -- print all the stocks held in the database
* quit -- shutdown this node
* exit -- shutdown this node
* stock_symbol number -- enter this stock and number into the database
*/
namespace excs_repquote
{
public class RepQuoteExample
{
private RepQuoteEnvironment dbenv;
private Thread checkpointThread;
private Thread logArchiveThread;
public static void usage()
{
Console.WriteLine(
"usage: " + RepConfig.progname +
" -h home -l|-L host:port\n" +
" [-CM][-r host:port][-R host:port]\n" +
" [-a all|quorum][-b][-p priority][-v]");
Console.WriteLine(
"\t -h home (required; h stands for home directory)\n" +
"\t -l host:port (required unless -L is specified;" +
" l stands for local)\n" +
"\t -L host:port (optional, L means group creator)" +
"\t -C or -M (optional; start up as client or master)\n" +
"\t -r host:port (optional; r stands for remote; any number " +
"of these\n" +
"\t may be specified)\n" +
"\t -R host:port (optional; R stands for remote peer; only " +
"one of\n" +
"\t these may be specified)\n" +
"\t -a all|quorum (optional; a stands for ack policy)\n" +
"\t -b (optional; b stands for bulk)\n" +
"\t -p priority (optional; defaults to 100)\n" +
"\t -v (optional; v stands for verbose)\n");
Environment.Exit(1);
}
public static void Main(string[] args)
{
RepConfig config = new RepConfig();
bool isPeer, isCreator;
uint tmpPort = 0;
/*
* RepQuoteExample is meant to be run from build_windows\AnyCPU, in
* either the Debug or Release directory. The required core
* libraries, however, are in either build_windows\Win32 or
* build_windows\x64, depending upon the platform. That location
* needs to be added to the PATH environment variable for the
* P/Invoke calls to work.
*/
try {
String pwd = Environment.CurrentDirectory;
pwd = Path.Combine(pwd, "..");
pwd = Path.Combine(pwd, "..");
if (IntPtr.Size == 4)
pwd = Path.Combine(pwd, "Win32");
else
pwd = Path.Combine(pwd, "x64");
#if DEBUG
pwd = Path.Combine(pwd, "Debug");
#else
pwd = Path.Combine(pwd, "Release");
#endif
pwd += ";" + Environment.GetEnvironmentVariable("PATH");
Environment.SetEnvironmentVariable("PATH", pwd);
} catch (Exception e) {
Console.WriteLine(
"Unable to set the PATH environment variable.");
Console.WriteLine(e.Message);
return;
}
/* Extract the command line parameters. */
for (int i = 0; i < args.Length; i++)
{
isPeer = false;
isCreator = false;
string s = args[i];
if (s[0] != '-')
continue;
switch (s[1])
{
case 'a':
if (i == args.Length - 1)
usage();
i++;
if (args[i].Equals("all"))
config.ackPolicy = AckPolicy.ALL;
else if (!args[i].Equals("quorum"))
usage();
break;
case 'b':
config.bulk = true;
break;
case 'C':
config.startPolicy = StartPolicy.CLIENT;
break;
case 'h':
if (i == args.Length - 1)
usage();
i++;
config.home = args[i];
break;
case 'l':
case 'L':
if (i == args.Length - 1)
usage();
if (args[i].Equals("-L"))
isCreator = true;
i++;
string[] words = args[i].Split(':');
if (words.Length != 2)
{
Console.Error.WriteLine("Invalid host " +
"specification host:port needed.");
usage();
}
try
{
tmpPort = uint.Parse(words[1]);
} catch (InvalidCastException)
{
Console.Error.WriteLine("Invalid host " +
"specification, could not parse port number.");
usage();
}
config.localSite.Host = words[0];
config.localSite.Port = tmpPort;
config.localSite.GroupCreator = isCreator;
config.localSite.LocalSite = true;
break;
case 'M':
config.startPolicy = StartPolicy.MASTER;
break;
case 'p':
if (i == args.Length - 1)
usage();
i++;
try
{
config.priority = uint.Parse(args[i]);
} catch (InvalidCastException)
{
Console.Error.WriteLine("Unable to parse priority.");
usage();
}
break;
case 'r':
case 'R':
if (i == args.Length - 1)
usage();
if (args[i].Equals("-R"))
isPeer = true;
i++;
words = args[i].Split(':');
if (words.Length != 2)
{
Console.Error.WriteLine("Invalid host " +
"specification host:port needed.");
usage();
}
try
{
tmpPort = uint.Parse(words[1]);
} catch (InvalidCastException)
{
Console.Error.WriteLine("Invalid host " +
"specification, could not parse port number.");
usage();
}
DbSiteConfig remoteConfig = new DbSiteConfig();
remoteConfig.Helper = true;
remoteConfig.Host = words[0];
remoteConfig.Port = tmpPort;
remoteConfig.Peer = isPeer;
config.remoteSites.Add(remoteConfig);
break;
case 'v':
config.verbose = true;
break;
default:
Console.Error.WriteLine(
"Unrecognized option: " + args[i]);
usage();
break;
}
}
/* Error check command line. */
if (config.localSite.Host == null || config.home.Length == 0)
usage();
RepQuoteExample runner = null;
try
{
runner = new RepQuoteExample();
runner.init(config);
runner.doloop();
runner.terminate();
runner = null;
} catch (DatabaseException dbErr)
{
Console.Error.WriteLine("Caught an exception during " +
"initialization or processing: " + dbErr);
if (runner != null)
runner.terminate();
}
} /* End main. */
public RepQuoteExample()
{
dbenv = null;
}
public int init(RepConfig config)
{
int ret = 0;
DatabaseEnvironmentConfig envConfig = new DatabaseEnvironmentConfig();
envConfig.ErrorPrefix = RepConfig.progname;
envConfig.RepSystemCfg = new ReplicationConfig();
envConfig.RepSystemCfg.RepmgrSitesConfig.Add(config.localSite);
for (int i = 0; i < config.remoteSites.Count; i++)
envConfig.RepSystemCfg.RepmgrSitesConfig.Add(config.remoteSites[i]);
envConfig.RepSystemCfg.BulkTransfer = config.bulk;
/*
* Configure heartbeat timeouts so that repmgr monitors the
* health of the TCP connection. Master sites broadcast a heartbeat
* at the frequency specified by the DB_REP_HEARTBEAT_SEND timeout.
* Client sites wait for message activity the length of the
* DB_REP_HEARTBEAT_MONITOR timeout before concluding that the
* connection to the master is lost. The DB_REP_HEARTBEAT_MONITOR
* timeout should be longer than the DB_REP_HEARTBEAT_SEND timeout.
*/
envConfig.RepSystemCfg.HeartbeatMonitor = 10000000;
envConfig.RepSystemCfg.HeartbeatSend = 5000000;
/*
* Set replication group election priority for this environment.
* An election first selects the site with the most recent log
* records as the new master. If multiple sites have the most
* recent log records, the site with the highest priority value
* is selected as master.
*/
envConfig.RepSystemCfg.Priority = config.priority;
envConfig.MPoolSystemCfg = new MPoolConfig();
envConfig.MPoolSystemCfg.CacheSize = RepConfig.CACHESIZE;
envConfig.TxnNoSync = true;
envConfig.EventNotify = new EventNotifyDelegate(RepQuoteEventHandler);
/*
* Set the policy that determines how master and client sites
* handle acknowledgement of replication messages needed for
* permanent records. The default policy of "quorum" requires only
* a quorum of electable peers sufficient to ensure a permanent
* record remains durable if an election is held. The "all" option
* requires all clients to acknowledge a permanent replication
* message instead.
*/
envConfig.RepSystemCfg.RepMgrAckPolicy = config.ackPolicy;
/*
* Set the threshold for the minimum and maximum time the client
* waits before requesting retransmission of a missing message.
* Base these values on the performance and load characteristics
* of the master and client host platforms as well as the round
* trip message time.
*/
envConfig.RepSystemCfg.RetransmissionRequest(20000, 500000);
/*
* Configure deadlock detection to ensure that any deadlocks
* are broken by having one of the conflicting lock requests
* rejected. DB_LOCK_DEFAULT uses the lock policy specified
* at environment creation time or DB_LOCK_RANDOM if none was
* specified.
*/
envConfig.LockSystemCfg = new LockingConfig();
envConfig.LockSystemCfg.DeadlockResolution = DeadlockPolicy.DEFAULT;
envConfig.Create = true;
envConfig.RunRecovery = true;
envConfig.FreeThreaded = true;
envConfig.UseReplication = true;
envConfig.UseLocking = true;
envConfig.UseLogging = true;
envConfig.UseMPool = true;
envConfig.UseTxns = true;
envConfig.Verbosity = new VerboseMessages();
envConfig.Verbosity.Replication = config.verbose;
try
{
dbenv = RepQuoteEnvironment.Open(config.home, envConfig);
} catch(DatabaseException e)
{
Console.WriteLine("Fail to open environment: " + e.Message);
return 1;
}
/* The following base replication features may also be useful to your
* application. See Berkeley DB documentation for more details.
* - Master leases: Provide stricter consistency for data reads
* on a master site.
* - Timeouts: Customize the amount of time Berkeley DB waits
* for such things as an election to be concluded or a master
* lease to be granted.
* - Delayed client synchronization: Manage the master site's
* resources by spreading out resource-intensive client
* synchronizations.
* - Blocked client operations: Return immediately with an error
* instead of waiting indefinitely if a client operation is
* blocked by an ongoing client synchronization.
*
* The following repmgr features may also be useful to your
* application. See Berkeley DB documentation for more details.
* - Two-site strict majority rule - In a two-site replication
* group, require both sites to be available to elect a new
* master.
* - Timeouts - Customize the amount of time repmgr waits
* for such things as waiting for acknowledgements or attempting
* to reconnect to other sites.
* - Site list - return a list of sites currently known to repmgr.
*/
/* Start checkpoint and log archive support threads. */
checkpointThread = new Thread(new ThreadStart(CheckPoint));
checkpointThread.Start();
logArchiveThread = new Thread(new ThreadStart(LogArchive));
logArchiveThread.Start();
/* Start replication manager. */
if (config.startPolicy == StartPolicy.CLIENT)
dbenv.env.RepMgrStartClient(3);
else if (config.startPolicy == StartPolicy.ELECTION)
dbenv.env.RepMgrStartClient(3, true);
else if (config.startPolicy == StartPolicy.MASTER)
dbenv.env.RepMgrStartMaster(3);
return ret;
}
public int doloop()
{
BTreeDatabase db = null;
for (;;)
{
if (db == null)
{
BTreeDatabaseConfig dbConfig = new BTreeDatabaseConfig();
dbConfig.Env = dbenv.env;
if (dbenv.IsMaster)
{
/*
* Open database allowing create only if this is a master
* database. A client database uses polling to attempt
* to open the database, without creating it, until the
* open succeeds.
*
* This polling logic for allowing create can be
* simplified under some circumstances. For example, if
* the application can be sure a database is already
* there, it would never need to open it allowing create.
*/
dbConfig.Creation = CreatePolicy.IF_NEEDED;
}
dbConfig.AutoCommit = true;
try
{
db = BTreeDatabase.Open(RepConfig.progname, dbConfig);
} catch (DatabaseException)
{
Console.WriteLine("no stock database available yet.");
if (db != null)
{
db.Close(true);
db = null;
}
Thread.Sleep(RepConfig.SLEEPTIME);
continue;
}
}
/* Listen for input, and add it to the database. */
Console.Write("QUOTESERVER");
if (dbenv.IsMaster == false)
Console.Write("(read-only)");
Console.Write("> ");
string nextLine = null;
try
{
nextLine = Console.ReadLine();
} catch(System.IO.IOException)
{
Console.WriteLine("Unable to get data");
break;
}
/* A blank line causes the DB to be dumped. */
string[] words = nextLine.Split(' ');
if (words.Length == 0 ||
words.Length == 1 && words[0].Length == 0)
{
try
{
if (dbenv.InClientSync)
Console.WriteLine("Cannot read data during " +
"client initialization - please try again.");
else
printStocks(db);
} catch (DeadlockException)
{
continue;
} catch (DatabaseException e)
{
/*
* This could be DB_REP_HANDLE_DEAD, which
* should close the database and continue.
*/
Console.WriteLine("Got db exception reading replication"
+ "DB: " + e);
Console.WriteLine("Expected if it was due to a dead " +
"replication handle, otherwise an unexpected error.");
db.Close(false);/* Close no sync. */
db = null;
continue;
}
continue;
}
if (words.Length == 1 &&
(words[0].ToLower().Equals("quit") ||
words[0].ToLower().Equals("exit")))
{
dbenv.AppFinished = true;
break;
} else if (words.Length != 2)
{
Console.WriteLine("Format: TICKER VALUE");
continue;
}
if (!dbenv.IsMaster)
{
Console.WriteLine("Can't update client");
continue;
}
DatabaseEntry key = new DatabaseEntry(
ASCIIEncoding.ASCII.GetBytes(words[0]));
DatabaseEntry data = new DatabaseEntry(
ASCIIEncoding.ASCII.GetBytes(words[1]));
db.Put(key, data);
}
if (db != null)
db.Close(true);
return 0;
}
public void terminate()
{
/* Wait for checkpoint and log archive threads to finish. */
try
{
logArchiveThread.Join();
checkpointThread.Join();
} catch (Exception)
{
Console.WriteLine("Support thread join failed.");
}
/*
* We have used the DB_TXN_NOSYNC environment flag for improved
* performance without the usual sacrifice of transactional durability,
* as discussed in the "Transactional guarantees" page of the Reference
* Guide: if one replication site crashes, we can expect the data to
* exist at another site. However, in case we shut down all sites
* gracefully, we push out the end of the log here so that the most
* recent transactions don't mysteriously disappear.
*/
dbenv.env.LogFlush();
dbenv.env.Close();
}
/*
* void return type since error conditions are propogated
* via exceptions.
*/
private void printStocks(Database db)
{
Cursor dbc = db.Cursor();
Console.WriteLine("\tSymbol\t\tPrice");
Console.WriteLine("\t======\t\t=====");
DatabaseEntry key = new DatabaseEntry();
DatabaseEntry data = new DatabaseEntry();
foreach (KeyValuePair<DatabaseEntry, DatabaseEntry> pair in dbc)
{
string keyStr = ASCIIEncoding.ASCII.GetString(pair.Key.Data);
string dataStr = ASCIIEncoding.ASCII.GetString(pair.Value.Data);
Console.WriteLine("\t"+keyStr+"\t\t"+dataStr);
}
dbc.Close();
}
/*
* This is a very simple thread that performs checkpoints at a fixed
* time interval. For a master site, the time interval is one minute
* plus the duration of the checkpoint_delay timeout (30 seconds by
* default.) For a client site, the time interval is one minute.
*/
public void CheckPoint()
{
for (; ; )
{
/*
* Wait for one minute, polling once per second to see if
* application has finished. When application has finished,
* terminate this thread.
*/
for (int i = 0; i < 60; i++)
{
Thread.Sleep(1000);
if (dbenv.AppFinished)
return;
}
/* Perform a checkpoint. */
try
{
dbenv.env.Checkpoint();
} catch (DatabaseException)
{
Console.WriteLine("Could not perform checkpoint.");
}
}
}
/*
* This is a simple log archive thread. Once per minute, it removes all but
* the most recent 3 logs that are safe to remove according to a call to
* DBENV->log_archive().
*
* Log cleanup is needed to conserve disk space, but aggressive log cleanup
* can cause more frequent client initializations if a client lags too far
* behind the current master. This can happen in the event of a slow client,
* a network partition, or a new master that has not kept as many logs as the
* previous master.
*
* The approach in this routine balances the need to mitigate against a
* lagging client by keeping a few more of the most recent unneeded logs
* with the need to conserve disk space by regularly cleaning up log files.
* Use of automatic log removal (DBENV->log_set_config() DB_LOG_AUTO_REMOVE
* flag) is not recommended for replication due to the risk of frequent
* client initializations.
*/
public void LogArchive()
{
int logKeep = 3;
int minLog;
List<string> logFileList;
for (; ; )
{
/*
* Wait for one minute, polling once per second to see if
* application has finished. When application has finished,
* terminate this thread.
*/
for (int i = 0; i < 60; i++)
{
Thread.Sleep(1000);
if (dbenv.AppFinished)
return;
}
try
{
/* Get the list of unneeded log files. */
logFileList = dbenv.env.ArchivableLogFiles(true);
/*
* Remove all but the logs_to_keep most recent unneeded
* log files.
*/
minLog = logFileList.Count - logKeep;
for (int i = 0; i < minLog; i++)
{
FileInfo logFile = new FileInfo(logFileList[i]);
logFile.Delete();
}
} catch (DatabaseException)
{
Console.WriteLine("Problem deleting log archive files.");
}
}
}
/*
* Implemention of EventHandler interface to handle the Berkeley DB events
* we are interested in receiving.
*/
private void RepQuoteEventHandler(NotificationEvent eventCode, byte[] event_info)
{
switch (eventCode)
{
case NotificationEvent.REP_CLIENT:
dbenv.IsMaster = false;
dbenv.InClientSync = true;
break;
case NotificationEvent.REP_MASTER:
dbenv.IsMaster = true;
dbenv.InClientSync = false;
break;
case NotificationEvent.REP_NEWMASTER:
dbenv.InClientSync = true;
break;
case NotificationEvent.REP_STARTUPDONE:
dbenv.InClientSync = false;
break;
/*
* Did not get enough acks to guarantee transaction
* durability based on the configured ack policy. This
* transaction will be flushed to the master site's
* local disk storage for durability.
*/
case NotificationEvent.REP_PERM_FAILED:
Console.WriteLine("Insufficient acknowledgements " +
"to guarantee transaction durability.");
break;
default:
Console.WriteLine("Ignoring event: {0}", eventCode);
break;
}
}
}
}