├── .gitignore ├── README.md ├── build.sbt ├── config └── development.scala ├── lib └── sbt-launch.jar ├── project ├── build.properties ├── build │ └── StarterProject.scala └── plugins.sbt ├── sbt ├── scripts └── serviceClient.rb └── src ├── main └── scala │ └── net │ └── mobocracy │ └── starter │ ├── Main.scala │ ├── RequestHash.scala │ ├── StarterService.scala │ ├── StarterServiceServer.scala │ ├── StringCodec.scala │ ├── client │ └── Main.scala │ ├── config │ └── StarterServiceConfig.scala │ └── util │ └── Helpers.scala └── test └── scala └── net └── mobocracy └── starter ├── AbstractSpec.scala └── StarterSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | target 3 | lib_managed 4 | src_managed 5 | project/boot/ 6 | dist 7 | *.log 8 | project/plugins/project 9 | *.gem 10 | .NERDTreeBookmarks 11 | sbt-launch.jar 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About and References 2 | 3 | Some finagle sample code for demonstration purposes. 4 | 5 | * [Finagle](https://github.com/twitter/finagle) - A fault tolerant, protocol-agnostic RPC system 6 | * [Overview](http://twitter.github.com/finagle/) 7 | * [Article](http://engineering.twitter.com/2011/08/finagle-protocol-agnostic-rpc-system.html) 8 | * [Scala Days 2011 Video](http://days2011.scala-lang.org/node/138/286) 9 | * [ACM Web Frameworks](http://steve.vinoski.net/pdf/IC-Scala_Web_Frameworks.pdf) 10 | * [Netty](http://www.jboss.org/netty) - an asynchronous event-driven network application framework 11 | * [Ostrich Project](https://github.com/twitter/ostrich) - stats collector & reporter for scala servers 12 | * [Scala School](http://twitter.github.com/scala_school/finagle.html) - wonderful tutorial produced by Twitter 13 | 14 | ## Running 15 | 16 | Note, if you don't work at Tumblr you will need to do the following before 17 | running sbt: 18 | 19 | $ export SBT_NO_PROXY=true 20 | 21 | Now to run things... 22 | 23 | ``` 24 | $ ./sbt 25 | > update 26 | > test 27 | 28 | ...snip... 29 | [info] == net.mobocracy.starter.StarterSpec == 30 | [info] + StarterService should 31 | [info] + accept a string 32 | [info] + throw an exception on magic string 33 | [info] + StarterServiceServer should 34 | [info] + throw an exception if 35 | [info] + no port is specified 36 | [info] + no name is specified 37 | [info] + shutdown properly 38 | ...snip... 39 | 40 | > run -f config/development.scala 41 | $ curl http://localhost:9900/shutdown.txt 42 | > exit 43 | ``` 44 | 45 | ## Configuration 46 | 47 | Look in the config directory. You can tweak logging options, ports, etc. 48 | 49 | ## Ostrich 50 | 51 | $ ./sbt 52 | > run -f config/development.scala 53 | $ ./scripts/serviceClient.rb "hello world" 54 | $ ./scripts/serviceClient.rb "please throw an exception" 55 | $ curl http://localhost:9900/stats.txt 56 | $ curl http://localhost:9900/shutdown.txt 57 | 58 | ### Output from above 59 | 60 | ``` 61 | $ ./scripts/serviceClient.rb "hello world" 62 | Sent: hello world 63 | Received: hello world 309ecc489c12d6eb4cc40f50c902f2b4d0ed77ee511a7c7a9bcd3ca86d4cd86f989dd35bc5ff499670da34255b45b0cfd830e81f605dcf7dc5542e93ae9cd76f 64 | 65 | $ ./scripts/serviceClient.rb "please throw an exception" 66 | Caught exception sending data, is the server listening? 67 | Exception: end of file reached 68 | 69 | $ curl http://localhost:9900/stats.txt 70 | counters: 71 | StarterService/connects: 2 72 | StarterService/failures/java.lang.Exception: 1 73 | StarterService/requests: 2 74 | StarterService/success: 1 75 | jvm_gc_ConcurrentMarkSweep_cycles: 0 76 | jvm_gc_ConcurrentMarkSweep_msec: 0 77 | jvm_gc_ParNew_cycles: 3 78 | jvm_gc_ParNew_msec: 127 79 | jvm_gc_cycles: 3 80 | jvm_gc_msec: 127 81 | service_request_count: 2 82 | gauges: 83 | StarterService/connections: 0 84 | StarterService/pending: 0 85 | jvm_fd_count: 107 86 | jvm_fd_limit: 10240 87 | jvm_heap_committed: 4214489088 88 | jvm_heap_max: 4214489088 89 | jvm_heap_used: 47287904 90 | jvm_nonheap_committed: 72814592 91 | jvm_nonheap_max: 1124073472 92 | jvm_nonheap_used: 71789944 93 | jvm_num_cpus: 8 94 | jvm_post_gc_CMS_Old_Gen_used: 0 95 | jvm_post_gc_CMS_Perm_Gen_used: 0 96 | jvm_post_gc_Par_Eden_Space_used: 0 97 | jvm_post_gc_Par_Survivor_Space_used: 8638904 98 | jvm_post_gc_used: 8638904 99 | jvm_start_time: 1315411957719 100 | jvm_thread_count: 13 101 | jvm_thread_daemon_count: 8 102 | jvm_thread_peak_count: 14 103 | jvm_uptime: 211539 104 | labels: 105 | metrics: 106 | StarterService/connection_duration: (average=21, count=2, maximum=35, minimum=6, p25=6, p50=6, p75=35, p90=35, p95=35, p99=35, p999=35, p9999=35, sum=42) 107 | StarterService/connection_received_bytes: (average=19, count=2, maximum=26, minimum=12, p25=12, p50=12, p75=26, p90=26, p95=26, p99=26, p999=26, p9999=26, sum=38) 108 | StarterService/connection_requests: (average=1, count=2, maximum=1, minimum=1, p25=1, p50=1, p75=1, p90=1, p95=1, p99=1, p999=1, p9999=1, sum=2) 109 | StarterService/connection_sent_bytes: (average=70, count=2, maximum=142, minimum=0, p25=0, p50=0, p75=142, p90=142, p95=142, p99=142, p999=142, p9999=142, sum=141) 110 | StarterService/request_latency_ms: (average=5, count=2, maximum=8, minimum=2, p25=2, p50=2, p75=8, p90=8, p95=8, p99=8, p999=8, p9999=8, sum=10) 111 | request_hash_msec: (average=1, count=2, maximum=2, minimum=0, p25=0, p50=0, p75=2, p90=2, p95=2, p99=2, p999=2, p9999=2, sum=2) 112 | 113 | $ curl http://localhost:9900/shutdown.txt 114 | ok 115 | ``` 116 | 117 | ## ClientBuilder client 118 | 119 | There is an alternative client (from the ruby one) that can be run via sbt or 120 | standalone. In the sbt shell you can run it as: 121 | 122 | ``` 123 | > run-class net.mobocracy.starter.client.Main --value "my test text" 124 | ``` 125 | 126 | The scala client takes the following arguments: 127 | 128 | * --concurrent - how many simultaneous connections to the server, defaults to 4 129 | * --port - the port to use, defaults to 4242 130 | * --requests - number of requests to make, defaults to 25 131 | * --timeout - the amount of time in seconds to wait on a reply 132 | * --value - the echo value to use, required 133 | 134 | ## Future Updates 135 | 136 | * Runtime heap profiling via [heapster](https://github.com/mariusaeriksen/heapster) 137 | * JMX example 138 | 139 | ## Favorite quotes 140 | 141 | > These methods are not generic enough for general use. 142 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | organization := "net.mobocracy" 2 | 3 | name := "starter" 4 | 5 | version := "1.2" 6 | 7 | scalaVersion := "2.9.2" 8 | 9 | scalacOptions += "-deprecation" 10 | 11 | resolvers ++= Seq("sonatype" at "https://oss.sonatype.org/content/groups/scala-tools/", 12 | "twitter.com" at "http://maven.twttr.com/") 13 | 14 | libraryDependencies ++= Seq( 15 | "com.twitter" % "finagle-core" % "6.6.2", 16 | "com.twitter" % "util-core" % "6.5.0", 17 | "com.twitter" % "finagle-ostrich4" % "6.6.2", 18 | "org.scala-tools.testing" %% "specs" % "1.6.9", 19 | "org.jmock" % "jmock-legacy" % "2.5.1" % "test") 20 | 21 | -------------------------------------------------------------------------------- /config/development.scala: -------------------------------------------------------------------------------- 1 | import net.mobocracy.starter.config._ 2 | import com.twitter.conversions.time._ 3 | import com.twitter.logging.config._ 4 | import com.twitter.ostrich.admin.config._ 5 | 6 | new StarterServiceConfig { 7 | port = 4242 8 | name = "StarterService" 9 | 10 | admin.httpPort = 9900 11 | 12 | admin.statsNodes = new StatsConfig { 13 | reporters = new JsonStatsLoggerConfig { 14 | loggerName = "stats" 15 | serviceName = name 16 | } :: new TimeSeriesCollectorConfig 17 | } 18 | 19 | loggers = 20 | new LoggerConfig { 21 | level = Level.TRACE 22 | handlers = new ConsoleHandlerConfig { 23 | formatter = new FormatterConfig { 24 | useFullPackageNames = true 25 | prefix = "%s [] %s: " 26 | } 27 | } 28 | } :: new LoggerConfig { 29 | node = "stats" 30 | level = Level.FATAL 31 | useParents = false 32 | handlers = new ConsoleHandlerConfig 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /lib/sbt-launch.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmatheny/finagle-starter-kit/a521fb8c71606de4e150f3e3c71e30dff190de05/lib/sbt-launch.jar -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmatheny/finagle-starter-kit/a521fb8c71606de4e150f3e3c71e30dff190de05/project/build.properties -------------------------------------------------------------------------------- /project/build/StarterProject.scala: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bmatheny/finagle-starter-kit/a521fb8c71606de4e150f3e3c71e30dff190de05/project/build/StarterProject.scala -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers ++= Seq( 2 | // "ibiblio" at "http://mirrors.ibiblio.org/pub/mirrors/maven2/", 3 | "twitter.com" at "http://maven.twttr.com/" 4 | // "powermock-api" at "http://powermock.googlecode.com/svn/repo/", 5 | // "scala-tools.org" at "http://scala-tools.org/repo-releases/", 6 | // "sonatype" at "https://oss.sonatype.org/content/groups/scala-tools/", 7 | // "testing.scala-tools.org" at "http://scala-tools.org/repo-releases/testing/", 8 | // "oauth.net" at "http://oauth.googlecode.com/svn/code/maven", 9 | // "download.java.net" at "http://download.java.net/maven/2/", 10 | // "atlassian" at "https://m2proxy.atlassian.com/repository/public/", 11 | // for netty: 12 | // "jboss" at "http://repository.jboss.org/nexus/content/groups/public/" 13 | ) 14 | 15 | addSbtPlugin("com.twitter" % "sbt-package-dist" % "1.1.1") 16 | -------------------------------------------------------------------------------- /sbt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | root=$( 4 | cd $(dirname $(readlink $0 || echo $0))/.. 5 | /bin/pwd 6 | ) 7 | 8 | sbtjar=sbt-launch.jar 9 | 10 | if [ ! -f $sbtjar ]; then 11 | echo 'downloading '$sbtjar 1>&2 12 | curl -O http://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.12.4/$sbtjar 13 | fi 14 | 15 | test -f $sbtjar || exit 1 16 | 17 | sbtjar_md5=$(openssl md5 < $sbtjar|cut -f2 -d'='|awk '{print $1}') 18 | 19 | if [ "${sbtjar_md5}" != ad8d9e114a5613ab2f439f1e4f8d542b ]; then 20 | echo 'bad sbtjar!' 1>&2 21 | exit 1 22 | fi 23 | 24 | test -f ~/.sbtconfig && . ~/.sbtconfig 25 | 26 | java -ea \ 27 | $SBT_OPTS \ 28 | $JAVA_OPTS \ 29 | -Djava.net.preferIPv4Stack=true \ 30 | -XX:+AggressiveOpts \ 31 | -XX:+UseParNewGC \ 32 | -XX:+UseConcMarkSweepGC \ 33 | -XX:+CMSParallelRemarkEnabled \ 34 | -XX:+CMSClassUnloadingEnabled \ 35 | -XX:MaxPermSize=1024m \ 36 | -XX:SurvivorRatio=128 \ 37 | -XX:MaxTenuringThreshold=0 \ 38 | -Xss8M \ 39 | -Xms512M \ 40 | -Xmx3G \ 41 | -server \ 42 | -jar $sbtjar "$@" 43 | -------------------------------------------------------------------------------- /scripts/serviceClient.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | require 'socket' 4 | require 'optparse' 5 | 6 | options = { 7 | :port => 4242 8 | } 9 | opts = OptionParser.new do |opts| 10 | opts.banner = "Usage: serviceClient.rb [option] text" 11 | 12 | opts.on("-p", "--port PORT", Integer, "Use the specified port for connecting to the service") do |port| 13 | options[:port] = port.to_i 14 | end 15 | end 16 | 17 | def client(data, port = PORT) 18 | begin 19 | sock = TCPsocket.new('127.0.0.1', port) 20 | sock.write(data + "\n") 21 | reply = sock.readline 22 | puts("Sent: #{data}") 23 | puts("Received: #{reply}") 24 | sock.close 25 | rescue Exception => e 26 | puts("Caught exception sending data, is the server listening?") 27 | puts("Exception: #{e.message}") 28 | end 29 | end 30 | 31 | opts.parse! 32 | if ( ARGV.length == 0 ) then 33 | puts(opts) 34 | exit 35 | else 36 | client(ARGV[0], options[:port]) 37 | end 38 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/Main.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import com.twitter.logging.Logger 4 | import com.twitter.ostrich.admin.{RuntimeEnvironment, ServiceTracker} 5 | 6 | object Main { 7 | val log = Logger.get(getClass) 8 | 9 | def main(args: Array[String]) { 10 | val runtime = RuntimeEnvironment(this, args) 11 | val server = runtime.loadRuntimeConfig[StarterServiceServer] 12 | try { 13 | log.info("Starting service") 14 | server.start() 15 | } catch { 16 | case e: Exception => 17 | log.error(e, "Failed starting service, exiting") 18 | ServiceTracker.shutdown() 19 | sys.exit(1) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/RequestHash.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import java.security.MessageDigest 4 | 5 | case class RequestHash(original: String, hash: String = "SHA-512") { 6 | require(original != null, "Can not hash null string") 7 | private val hasher = MessageDigest.getInstance(hash) 8 | val hashed = { 9 | hasher.update(original.getBytes("UTF-8")) 10 | hasher.digest.map { "%02x" format _ }.mkString 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/StarterService.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import scala.util.Random 4 | 5 | import java.net.InetSocketAddress 6 | import java.util.concurrent.Callable 7 | 8 | import com.twitter.conversions._ 9 | import com.twitter.finagle.{ClientConnection, Filter, Service, ServiceFactory, SimpleFilter} 10 | import com.twitter.finagle.builder.{Server, ServerBuilder} 11 | import com.twitter.finagle.stats.OstrichStatsReceiver 12 | import com.twitter.logging.Logger 13 | import com.twitter.ostrich.stats.Stats 14 | import com.twitter.util.{Future, Time} 15 | 16 | trait StarterService { 17 | val port: Int 18 | val name: String 19 | var server: Option[Server] = None 20 | 21 | val log = Logger.get(getClass) 22 | val MAX_SLEEP = 5000L // Random sleep in getStringHash 23 | val MIN_SLEEP = 1000L 24 | 25 | // Don't initialize until after mixed in by another class 26 | lazy val serverSpec = ServerBuilder() 27 | .codec(StringCodec()) 28 | .bindTo(new InetSocketAddress(port)) 29 | .name(name) 30 | .reportTo(new OstrichStatsReceiver) 31 | 32 | def quitCheck(client: ClientConnection) = new SimpleFilter[String, String] { 33 | def apply(request: String, service: Service[String, String]): Future[String] = { 34 | request match { 35 | case "quit" => 36 | if (client != null) client.close() 37 | Future.value("noop") // no clients see this 38 | case _ => 39 | service(request) 40 | } 41 | } 42 | } 43 | 44 | val exceptionCheck = new SimpleFilter[String,String] { 45 | def apply(request: String, service: Service[String, String]): Future[String] = { 46 | request match { 47 | case "please throw an exception" => 48 | Future.exception(new Exception("Asked to throw, don't blame me")) 49 | case _ => 50 | service(request) 51 | } 52 | } 53 | } 54 | 55 | val hashRequest = new Filter[String, String, RequestHash, String] { 56 | def apply(request: String, service: Service[RequestHash, String]): Future[String] = { 57 | val sleepTime = (math.abs(Random.nextLong()) % (MAX_SLEEP - MIN_SLEEP)) + MIN_SLEEP 58 | 59 | log.debug("Sleeping %d milliseconds for %s", sleepTime, request) 60 | 61 | val hash = Stats.time("request_hash") { 62 | Thread.sleep(sleepTime) 63 | RequestHash(request) 64 | } 65 | service(hash) 66 | } 67 | } 68 | 69 | val serviceImpl = new Service[RequestHash, String] { 70 | def apply(request: RequestHash): Future[String] = { 71 | log.debug("Got request %s", request) 72 | Stats.incr("service_request_count") 73 | Future.value(request.original + "\t" + request.hashed + "\n") 74 | } 75 | } 76 | 77 | val service = new ServiceFactory[String, String] { 78 | val underlying: Service[String, String] = exceptionCheck andThen hashRequest andThen serviceImpl 79 | override def apply(client: ClientConnection): Future[Service[String, String]] = { 80 | log.debug("Got request") 81 | Future.value(quitCheck(client) andThen underlying) 82 | } 83 | override def close(deadline: Time): Future[Unit] = { 84 | log.debug("Closing service factory in %s".format(deadline.toString())); 85 | Future.Unit 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/StarterServiceServer.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import config.StarterServiceConfig 4 | 5 | import com.twitter.conversions.time._ 6 | import com.twitter.logging.Logger 7 | import com.twitter.ostrich.admin.{Service => AdminService} 8 | 9 | class StarterServiceServer(config: StarterServiceConfig) extends AdminService with StarterService { 10 | require(config != null, "Config must be specified") 11 | require(config.port > 0, "Need a port to listen on") 12 | require(config.name != null && config.name.length > 0, "Need a service name") 13 | 14 | val port = config.port 15 | val name = config.name 16 | 17 | override def start() { 18 | log.debug("Starting StarterServiceServer %s on port %d", name, port) 19 | server = Some(serverSpec.build(service)) 20 | } 21 | 22 | override def shutdown() { 23 | log.debug("Shutdown requested") 24 | server match { 25 | case None => 26 | log.warning("Server not started, refusing to shutdown") 27 | case Some(server) => 28 | try { 29 | server.close(0.seconds) 30 | log.info("Shutdown complete") 31 | } catch { 32 | case e: Exception => 33 | log.error(e, "Error shutting down server %s listening on port %d", name, port) 34 | } 35 | } // server match 36 | } 37 | 38 | override def reload() { 39 | log.info("Reload requested, doing nothing but I could re-read the config or something") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/StringCodec.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import com.twitter.finagle.{Codec, CodecFactory} 4 | import org.jboss.netty.handler.codec.string.{StringEncoder, StringDecoder} 5 | import org.jboss.netty.channel.{Channels, ChannelPipelineFactory} 6 | import org.jboss.netty.handler.codec.frame.{Delimiters, DelimiterBasedFrameDecoder} 7 | import org.jboss.netty.util.CharsetUtil 8 | 9 | /** 10 | * A really simple demonstration of a custom Codec. This Codec is a newline (\n) 11 | * delimited line-based protocol. Here we re-use existing encoders/decoders as 12 | * provided by Netty. 13 | * 14 | * Taken from finagle/finagle-example/src/main/scala/com/twitter/finagle/example/echo 15 | */ 16 | object StringCodec extends StringCodec { 17 | def apply() = new StringCodec() 18 | } 19 | 20 | class StringCodec() extends CodecFactory[String, String] { 21 | def server = Function.const { 22 | new Codec[String, String] { 23 | def pipelineFactory = new ChannelPipelineFactory { 24 | def getPipeline = { 25 | val pipeline = Channels.pipeline() 26 | pipeline.addLast("line", 27 | new DelimiterBasedFrameDecoder(100, Delimiters.lineDelimiter: _*)) 28 | pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8)) 29 | pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8)) 30 | pipeline 31 | } 32 | } 33 | } 34 | } 35 | 36 | def client = Function.const { 37 | new Codec[String, String] { 38 | def pipelineFactory = new ChannelPipelineFactory { 39 | def getPipeline = { 40 | val pipeline = Channels.pipeline() 41 | pipeline.addLast("stringEncode", new StringEncoder(CharsetUtil.UTF_8)) 42 | pipeline.addLast("stringDecode", new StringDecoder(CharsetUtil.UTF_8)) 43 | pipeline 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/client/Main.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | package client 3 | 4 | import com.twitter.conversions.time._ 5 | import com.twitter.finagle.RequestTimeoutException 6 | import com.twitter.finagle.builder.ClientBuilder 7 | import com.twitter.util.{Await, Future, JavaTimer, Time, TimeoutException} 8 | 9 | object Main { 10 | def main(args: Array[String]) { 11 | val options = parseOptions(Map(), args.toList) 12 | require(options.get("value").isDefined, "--value must be specified") 13 | val concurrent = options.getOrElse("concurrent", 4).asInstanceOf[Int] 14 | val port = options.getOrElse("port", 4242).asInstanceOf[Int] 15 | val progress = options.getOrElse("progress", true).asInstanceOf[Boolean] 16 | val requestCount = options.getOrElse("requests", 25).asInstanceOf[Int] 17 | val value = options("value").asInstanceOf[String] 18 | val timeout = options.getOrElse("timeout", 4).asInstanceOf[Int].seconds 19 | val totalTimeout = options.getOrElse("totalTimeout", 100).asInstanceOf[Int].seconds 20 | implicit val timer = new JavaTimer 21 | 22 | val client = ClientBuilder() 23 | .codec(StringCodec()) 24 | .hosts("localhost:%d".format(port)) 25 | .name("StarterClient") 26 | .hostConnectionLimit(concurrent) 27 | .requestTimeout(timeout) 28 | .build() 29 | 30 | val requests = (0 until requestCount) map { i => 31 | val request = value + " " + i 32 | client(request + "\n") onSuccess { result => 33 | if (progress) printf("Received success result for %s: %s\n", request, result.stripLineEnd) 34 | } onFailure { error => 35 | if (progress) printf("Received error result for %s: %s\n", request, error.getClass.toString) 36 | } handle { case e: RequestTimeoutException => "Request Timeout for %s".format(request) } 37 | } 38 | 39 | Future.collect(requests).within(totalTimeout) onSuccess { list => 40 | println("\nResults\n") 41 | println(list.map { _.stripLineEnd } mkString("\n")) 42 | } onFailure { err => 43 | println("\nResults\n") 44 | println("Error processing list: " + err) 45 | } ensure { 46 | client.close(); 47 | } 48 | } 49 | 50 | // Inspired by http://goo.gl/ksaA5 51 | protected def parseOptions(parsed: Map[String, Any], list: List[String]): Map[String, Any] = list match { 52 | case Nil => parsed 53 | case "--concurrent" :: value :: tail => 54 | parseOptions(parsed ++ Map("concurrent" -> value.toInt), tail) 55 | case "--noprogress" :: tail => 56 | parseOptions(parsed ++ Map("progress" -> false), tail) 57 | case "--port" :: value :: tail => 58 | parseOptions(parsed ++ Map("port" -> value.toInt), tail) 59 | case "--requests" :: value :: tail => 60 | parseOptions(parsed ++ Map("requests" -> value.toInt), tail) 61 | case "--timeout" :: value :: tail => 62 | parseOptions(parsed ++ Map("timeout" -> value.toInt), tail) 63 | case "--totalTimeout" :: value :: tail => 64 | parseOptions(parsed ++ Map("totalTimeout" -> value.toInt), tail) 65 | case "--value" :: value :: tail => 66 | parseOptions(parsed ++ Map("value" -> value), tail) 67 | case unknownOption :: tail => 68 | println("Unknown option " + unknownOption) 69 | println("--concurrent INT - Max number of server connections, 4") 70 | println("--noprogress - Whether to display progress or not, true") 71 | println("--port INT - Port to connect to, 4242") 72 | println("--requests INT - Number of requests to issue, 25") 73 | println("--timeout INT - Seconds to wait before request timeout, 4") 74 | println("--totalTimeout INT - Seconds to wait before all requests are timed out, 30") 75 | println("--value STRING - Echo value to use, must be specified") 76 | sys.exit(1) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/config/StarterServiceConfig.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | package config 3 | 4 | import util.Helpers._ 5 | 6 | import com.twitter.ostrich.admin.RuntimeEnvironment 7 | 8 | class StarterServiceConfig { 9 | var port: Int = 3009 10 | var name: String = "StarterService" 11 | 12 | var runtime: RuntimeEnvironment = null 13 | 14 | def apply(_runtime: RuntimeEnvironment) = { 15 | require(port > 0, "Port must be specified and greater than 0") 16 | require(isPortAvailable(port) == true, "Already listening on port " + port) 17 | require(_runtime != null, "Need a runtime") 18 | require(name != null && name.length > 0, "Need a service name") 19 | 20 | runtime = _runtime 21 | 22 | new StarterServiceServer(this) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/scala/net/mobocracy/starter/util/Helpers.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | package util 3 | 4 | object Helpers { 5 | def isPortAvailable(port: Int): Boolean = { 6 | require(port > 0, "Port must be greater than 0") 7 | import java.net.{ConnectException, Socket} 8 | 9 | try { 10 | val socket = new Socket("localhost", port) 11 | socket.close() 12 | false 13 | } catch { 14 | case e:ConnectException => 15 | true 16 | case e:Exception => 17 | false 18 | } 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/scala/net/mobocracy/starter/AbstractSpec.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import com.twitter.logging.{ConsoleHandler, Logger, LoggerFactory} 4 | import com.twitter.logging.config._ 5 | 6 | import org.specs.Specification 7 | 8 | abstract class AbstractSpec extends Specification { 9 | 10 | def setLogLevel(_level: Level) { 11 | Logger.configure(List(LoggerFactory( 12 | level = Some(_level), 13 | handlers = List(ConsoleHandler()) 14 | ))) 15 | } 16 | 17 | setLogLevel(Level.WARNING) 18 | } 19 | -------------------------------------------------------------------------------- /src/test/scala/net/mobocracy/starter/StarterSpec.scala: -------------------------------------------------------------------------------- 1 | package net.mobocracy.starter 2 | 3 | import config.StarterServiceConfig 4 | 5 | import com.twitter.finagle.{ClientConnection, Service} 6 | import com.twitter.finagle.builder.Server 7 | import com.twitter.conversions.time._ 8 | import com.twitter.util.Await 9 | 10 | import org.specs.mock._ 11 | 12 | class StarterSpec extends AbstractSpec with JMocker { 13 | 14 | var testService: StarterService = _ 15 | val mockClientConnection = mock[ClientConnection] 16 | 17 | def exContains(ss: String): PartialFunction[Exception, Boolean] = { 18 | case ex: Exception => ex.getMessage.toLowerCase.contains(ss.toLowerCase) 19 | } 20 | 21 | def getService: Service[String, String] = { 22 | Await.result(testService.service(mockClientConnection)) 23 | } 24 | 25 | "StarterService" should { 26 | doBefore { 27 | testService = new { 28 | val port = 10000 29 | val name = "Test Server" 30 | } with StarterService; 31 | } 32 | 33 | "accept a string" >> { 34 | val future = getService("hello") 35 | val reply = Await.result(future) 36 | reply must beMatching("hello.*") 37 | } 38 | 39 | "handle a quit" >> { 40 | expect { 41 | one(mockClientConnection).close() 42 | } 43 | val future = getService("quit") 44 | val reply = Await.result(future) 45 | reply mustEqual("noop") 46 | } 47 | 48 | "throw an exception on magic string" >> { 49 | val future = getService("please throw an exception") 50 | val reply = Await.result(future) must throwA[Exception].like(exContains("don't blame")) 51 | } 52 | } 53 | 54 | "StarterServiceServer" should { 55 | "throw an exception if" >> { 56 | "no port is specified" >> { 57 | val config = new StarterServiceConfig {port = -1; name = "Hello"} 58 | new StarterServiceServer(config) must throwA[IllegalArgumentException].like(exContains("port")) 59 | } 60 | "no name is specified" >> { 61 | val config = new StarterServiceConfig {port = 1025; name = ""} 62 | new StarterServiceServer(config) must throwA[IllegalArgumentException].like(exContains("service name")) 63 | } 64 | } 65 | "shutdown properly" >> { 66 | val config = new StarterServiceConfig() 67 | val mockServer: Server = mock[Server] 68 | expect { 69 | one(mockServer).close(0.seconds) 70 | } 71 | val service = new StarterServiceServer(config) { 72 | server = Some(mockServer) 73 | } 74 | service.shutdown() 75 | } 76 | } // StarterServiceServer 77 | 78 | 79 | } 80 | --------------------------------------------------------------------------------