GitHub PullRequestReviewCommentEvent at 2014-06-07 01:44:38
Repository: spark
Actor: tdas
Payload: {"action":"created","comment":{"url":"https://api.github.com/repos/apache/spark/pulls/comments/13517122","id":13517122,"diff_hunk":"@@ -0,0 +1,410 @@\n+/*\n+ * Licensed to the Apache Software Foundation (ASF) under one or more\n+ * contributor license agreements.  See the NOTICE file distributed with\n+ * this work for additional information regarding copyright ownership.\n+ * The ASF licenses this file to You under the Apache License, Version 2.0\n+ * (the \"License\"); you may not use this file except in compliance with\n+ * the License.  You may obtain a copy of the License at\n+ *\n+ *    http://www.apache.org/licenses/LICENSE-2.0\n+ *\n+ * Unless required by applicable law or agreed to in writing, software\n+ * distributed under the License is distributed on an \"AS IS\" BASIS,\n+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n+ * See the License for the specific language governing permissions and\n+ * limitations under the License.\n+ */\n+\n+package org.apache.spark.util\n+\n+import java.io.{File, FileFilter, FileOutputStream, InputStream}\n+import java.text.SimpleDateFormat\n+import java.util.Calendar\n+import java.util.concurrent.{Executors, ThreadFactory}\n+\n+import org.apache.commons.io.FileUtils\n+import org.apache.spark.{Logging, SparkConf}\n+import RollingFileAppender._\n+\n+/**\n+ * Continuously appends the data from an input stream into the given file.\n+ */\n+private[spark] class FileAppender(inputStream: InputStream, file: File, bufferSize: Int = 8192)\n+  extends Logging {\n+  @volatile private var outputStream: FileOutputStream = null\n+  @volatile private var markedForStop = false     // has the appender been asked to stopped\n+  @volatile private var stopped = false           // has the appender stopped\n+\n+  // Thread that reads the input stream and writes to file\n+  private val writingThread = new Thread(\"File appending thread for \" + file) {\n+    setDaemon(true)\n+    override def run() {\n+      Utils.logUncaughtExceptions {\n+        appendStreamToFile()\n+      }\n+    }\n+  }\n+  writingThread.start()\n+\n+  /**\n+   * Wait for the appender to stop appending, either because input stream is closed\n+   * or because of any error in appending\n+   */\n+  def awaitTermination() {\n+    synchronized {\n+      if (!stopped) {\n+        wait()\n+      }\n+    }\n+  }\n+\n+  /** Stop the appender */\n+  def stop() {\n+    markedForStop = true\n+  }\n+\n+  /** Continuously read chunks from the input stream and append to the file */\n+  protected def appendStreamToFile() {\n+    try {\n+      logDebug(\"Started appending thread\")\n+      openFile()\n+      val buf = new Array[Byte](bufferSize)\n+      var n = 0\n+      while (!markedForStop && n != -1) {\n+        n = inputStream.read(buf)\n+        if (n != -1) {\n+          appendToFile(buf, n)\n+        }\n+      }\n+    } catch {\n+      case e: Exception =>\n+        logError(s\"Error writing stream to file $file\", e)\n+    } finally {\n+      closeFile()\n+      synchronized {\n+        stopped = true\n+        notifyAll()\n+      }\n+    }\n+  }\n+\n+  /** Append bytes to the file output stream */\n+  protected def appendToFile(bytes: Array[Byte], len: Int) {\n+    if (outputStream == null) {\n+      openFile()\n+    }\n+    outputStream.write(bytes, 0, len)\n+  }\n+\n+  /** Open the file output stream */\n+  protected def openFile() {\n+    outputStream = new FileOutputStream(file, true)\n+    logDebug(s\"Opened file $file\")\n+  }\n+\n+  /** Close the file output stream */\n+  protected def closeFile() {\n+    outputStream.flush()\n+    outputStream.close()\n+    logDebug(s\"Closed file $file\")\n+  }\n+}\n+\n+/**\n+ * Companion object to [[org.apache.log4j.FileAppender]] which has helper\n+ * functions to choose the write type of FileAppender based on SparkConf configuration.\n+ */\n+private[spark] object FileAppender extends Logging {\n+\n+  /** Create the right appender based on Spark configuration */\n+  def apply(inputStream: InputStream, file: File, conf: SparkConf): FileAppender = {\n+\n+    import RollingFileAppender._\n+\n+    val rolloverSizeOption = conf.getOption(SIZE_PROPERTY)\n+    val rolloverIntervalOption = conf.getOption(INTERVAL_PROPERTY)\n+\n+    (rolloverIntervalOption, rolloverSizeOption) match {\n+\n+      case (Some(rolloverInterval), Some(rolloverSize)) =>\n+        // if both size and interval have been set\n+        logWarning(s\"Rollover interval [$rolloverInterval] and size [$rolloverSize] \" +\n+          s\"both set for executor logs, rolling logs not enabled\")\n+        new FileAppender(inputStream, file)\n+\n+      case (Some(rolloverInterval), None) =>  // if interval has been set\n+        val validatedParams: Option[(Long, String)] = rolloverInterval match {\n+          case \"daily\" =>\n+            logInfo(s\"Rolling executor logs enabled for $file with daily rolling\")\n+            Some(24 * 60 * 60 * 1000L, s\"--YYYY-MM-dd\")\n+          case \"hourly\" =>\n+            logInfo(s\"Rolling executor logs enabled for $file with hourly rolling\")\n+            Some(60 * 60 * 1000L, s\"--YYYY-MM-dd--HH\")\n+          case \"minutely\" =>\n+            logInfo(s\"Rolling executor logs enabled for $file with rolling every minute\")\n+            Some(60 * 1000L, s\"--YYYY-MM-dd--HH-mm\")\n+          case IntParam(seconds) =>\n+            logInfo(s\"Rolling executor logs enabled for $file with rolling $seconds seconds\")\n+            Some(seconds * 1000L, s\"--YYYY-MM-dd--HH-mm-ss\")\n+          case _ =>\n+            logWarning(s\"Illegal interval for rolling executor logs [$rolloverInterval], \" +\n+                s\"rolling logs not enabled\")\n+            None\n+        }\n+        validatedParams.map {\n+          case (interval, pattern) =>\n+            new RollingFileAppender(\n+              inputStream, file, new TimeBasedRollingPolicy(interval),pattern, conf)\n+        }.getOrElse {\n+          new FileAppender(inputStream, file)\n+        }\n+\n+      case (None, Some(rolloverSize)) =>    // if size has been set\n+        rolloverSize match {\n+          case IntParam(bytes) =>\n+            logInfo(s\"Rolling executor logs enabled for $file with rolling every $bytes bytes\")\n+            new RollingFileAppender(inputStream, file, new SizeBasedRollingPolicy(bytes),\n+              s\"--YYYY-MM-dd--HH-mm-ss-SSSS\", conf)\n+          case _ =>\n+            logWarning(\n+              s\"Illegal size for rolling executor logs [$rolloverSize], \" +\n+                s\"rolling logs not enabled\")\n+            new FileAppender(inputStream, file)\n+        }\n+\n+      case (None, None) =>                // if neither size nor interval has been set\n+        new FileAppender(inputStream, file)\n+    }\n+  }\n+}\n+\n+/**\n+ * Defines the policy based on which [[org.apache.spark.util.RollingFileAppender]] will\n+ * generate rolling files.\n+ */\n+private[spark] trait RollingPolicy {\n+\n+  /** Whether rollover should be initiated at this moment */\n+  def shouldRollover(bytesToBeWritten: Long): Boolean\n+\n+  /** Notify that rollover has occurred */\n+  def rolledOver()\n+\n+  /** Notify that bytes have been written */\n+  def bytesWritten(bytes: Long)\n+}\n+\n+/**\n+ * Defines a [[org.apache.spark.util.RollingPolicy]] by which files will be rolled\n+ * over at a fixed interval.\n+ */\n+private[spark] class TimeBasedRollingPolicy(\n+    var rolloverIntervalMillis: Long,\n+    checkIntervalConstraint: Boolean = true   // set to false while testing\n+  ) extends RollingPolicy with Logging {\n+\n+  import TimeBasedRollingPolicy._\n+  if (checkIntervalConstraint && rolloverIntervalMillis < MINIMUM_INTERVAL_SECONDS * 1000L) {\n+    logWarning(s\"Rolling interval [${rolloverIntervalMillis/1000L} seconds] is too small. \" +\n+      s\"Setting the interval to the acceptable minimum of $MINIMUM_INTERVAL_SECONDS seconds.\")\n+    rolloverIntervalMillis = MINIMUM_INTERVAL_SECONDS * 1000L\n+  }\n+\n+  @volatile private var nextRolloverTime = calculateNextRolloverTime()\n+\n+  /** Should rollover if current time has exceeded next rollover time */\n+  def shouldRollover(bytesToBeWritten: Long): Boolean = {\n+    System.currentTimeMillis > nextRolloverTime\n+  }\n+\n+  /** Rollover has occurred, so find the next time to rollover */\n+  def rolledOver() {\n+    nextRolloverTime = calculateNextRolloverTime()\n+    logDebug(s\"Current time: ${System.currentTimeMillis}, next rollover time: \" + nextRolloverTime)\n+  }\n+\n+  def bytesWritten(bytes: Long) { }  // nothing to do\n+\n+  private def calculateNextRolloverTime(): Long = {\n+    val now = System.currentTimeMillis()\n+    val targetTime = (\n+      math.ceil(now.toDouble / rolloverIntervalMillis) * rolloverIntervalMillis\n+    ).toLong\n+    logDebug(s\"Next rollover time is $targetTime\")\n+    targetTime\n+  }\n+}\n+\n+private[spark] object TimeBasedRollingPolicy {\n+  val MINIMUM_INTERVAL_SECONDS = 60L  // 1 minute\n+}\n+\n+/**\n+ * Defines a [[org.apache.spark.util.RollingPolicy]] by which files will be rolled\n+ * over after reaching a particular size.\n+ */\n+private[spark] class SizeBasedRollingPolicy(\n+    var rolloverSizeBytes: Long,\n+    checkSizeConstraint: Boolean = true     // set to false while testing\n+  ) extends RollingPolicy with Logging {\n+\n+  import SizeBasedRollingPolicy._\n+  if (checkSizeConstraint && rolloverSizeBytes < MINIMUM_SIZE_BYTES) {\n+    logWarning(s\"Rolling size [$rolloverSizeBytes bytes] is too small. \" +\n+      s\"Setting the size to the acceptable minimum of $MINIMUM_SIZE_BYTES bytes.\")\n+    rolloverSizeBytes = MINIMUM_SIZE_BYTES * 1000L\n+  }\n+\n+  @volatile private var bytesWrittenTillNow = 0L\n+\n+  /** Should rollover if the next set of bytes is going to exceed the size limit */\n+  def shouldRollover(bytesToBeWritten: Long): Boolean = {\n+    bytesToBeWritten + bytesWrittenTillNow > rolloverSizeBytes\n+  }\n+\n+  /** Rollover has occurred, so reset the counter */\n+  def rolledOver() {\n+    bytesWrittenTillNow = 0\n+  }\n+\n+  /** Increment the bytes that have been written in the current file */\n+  def bytesWritten(bytes: Long) {\n+    bytesWrittenTillNow += bytes\n+  }\n+}\n+\n+private[spark] object SizeBasedRollingPolicy {\n+  val MINIMUM_SIZE_BYTES = RollingFileAppender.DEFAULT_BUFFER_SIZE * 10\n+}\n+\n+\n+/**\n+ * Continuously appends data from input stream into the given file, and rolls\n+ * over the file after the given interval. The rolled over files are named\n+ * based on the given pattern.\n+ *\n+ * @param inputStream             Input stream to read data from\n+ * @param activeFile              File to write data to\n+ * @param rollingPolicy           Policy based on which files will be rolled over.\n+ * @param rollingFilePattern      Pattern based on which the rolled over files will be named.\n+ *                                Uses SimpleDataFormat pattern.\n+ * @param conf                    SparkConf that is used to pass on extra configurations\n+ * @param bufferSize              Optional buffer size. Used mainly for testing.\n+ */\n+private[spark] class RollingFileAppender(\n+    inputStream: InputStream,\n+    activeFile: File,\n+    val rollingPolicy: RollingPolicy,\n+    rollingFilePattern: String,\n+    conf: SparkConf,\n+    bufferSize: Int = DEFAULT_BUFFER_SIZE\n+  ) extends FileAppender(inputStream, activeFile, bufferSize) {\n+\n+  private val retainCount = conf.getInt(KEEP_LAST_PROPERTY, -1)\n+  private val formatter = new SimpleDateFormat(rollingFilePattern)\n+\n+  private val executor = Executors.newFixedThreadPool(1, new ThreadFactory{\n+    def newThread(r: Runnable): Thread = {\n+      val t = new Thread(r)\n+      t.setDaemon(true)\n+      t.setName(s\"Threadpool of ${RollingFileAppender.this.getClass.getSimpleName} for $activeFile\")\n+      t\n+    }\n+  })\n+\n+  /** Stop the appender */\n+  override def stop() {\n+    super.stop()\n+    executor.shutdownNow()\n+  }\n+\n+  /** Append bytes to file after rolling over is necessary */\n+  override protected def appendToFile(bytes: Array[Byte], len: Int) {\n+    if (rollingPolicy.shouldRollover(len)) {\n+      rollover()\n+      rollingPolicy.rolledOver()\n+    }\n+    super.appendToFile(bytes, len)\n+    rollingPolicy.bytesWritten(len)\n+  }\n+\n+  /** Rollover the file, by closing the output stream and moving it over */\n+  private def rollover() {\n+    val rolloverSuffix = formatter.format(Calendar.getInstance.getTime)\n+    val rolloverFile = new File(\n+      activeFile.getParentFile, activeFile.getName + rolloverSuffix).getAbsoluteFile\n+    logDebug(\"Attempting to rollover at \" + System.currentTimeMillis + \" to file \" + rolloverFile)\n+\n+    try {\n+      closeFile()\n+      if (activeFile.exists) {\n+        if (!rolloverFile.exists) {\n+          FileUtils.moveFile(activeFile, rolloverFile)\n+          logInfo(s\"Rolled over $activeFile to $rolloverFile\")\n+        } else {\n+          // In case the rollover file name clashes, make a unique file name.\n+          // The resultant file names are long and ugly, so this is used only\n+          // if there is a name collision. This can be avoided by the using\n+          // the right pattern such that name collisions do not occur.\n+          var i = 0\n+          var altRolloverFile: File = null\n+          do {\n+            altRolloverFile = new File(activeFile.getParent,\n+              s\"${activeFile.getName}$rolloverSuffix--$i\").getAbsoluteFile\n+            i += 1\n+          } while (i < 10000 && altRolloverFile.exists)\n+\n+          logWarning(s\"Rollover file $rolloverFile already exists, \" +\n+            s\"rolled over $activeFile to file $altRolloverFile\")\n+          logWarning(s\"Make sure that the given file name pattern [$rollingFilePattern]\")\n+          FileUtils.moveFile(activeFile, altRolloverFile)\n+        }\n+      } else {\n+        logWarning(s\"File $activeFile does not exist\")\n+      }\n+      openFile()\n+      scheduleCleanup()\n+    } catch {\n+      case e: Exception =>\n+        logError(s\"Error rolling over $activeFile to $rolloverFile\", e)\n+    }\n+  }\n+\n+  /** Schedule cleaning up for older rolled over files */\n+  private def scheduleCleanup() {\n+    if (retainCount > 0) {\n+      executor.submit(new Runnable() { override def run() { cleanup() }})","path":"core/src/main/scala/org/apache/spark/util/FileAppender.scala","position":376,"original_position":376,"commit_id":"422440996e653ef5d77755909e64ada5d3160ee9","original_commit_id":"422440996e653ef5d77755909e64ada5d3160ee9","user":{"login":"tdas","id":663212,"avatar_url":"https://avatars.githubusercontent.com/u/663212?","gravatar_id":"dd4cdf7510ba6e04e617f6c3a3fc62f7","url":"https://api.github.com/users/tdas","html_url":"https://github.com/tdas","followers_url":"https://api.github.com/users/tdas/followers","following_url":"https://api.github.com/users/tdas/following{/other_user}","gists_url":"https://api.github.com/users/tdas/gists{/gist_id}","starred_url":"https://api.github.com/users/tdas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tdas/subscriptions","organizations_url":"https://api.github.com/users/tdas/orgs","repos_url":"https://api.github.com/users/tdas/repos","events_url":"https://api.github.com/users/tdas/events{/privacy}","received_events_url":"https://api.github.com/users/tdas/received_events","type":"User","site_admin":false},"body":"I was divided on that. I had it that way initially. I wanted to minimize the number of operations that i have to do because i am doing the rolling (closing file, renaming file, opening new file) on the data path (pausing the pulling from the stream and doing the rolling). If i delay more than necessary, I am not sure what the effect of pausing that stream-pulling would be. I can change it very easily if you think that's okay.","created_at":"2014-06-07T01:44:38Z","updated_at":"2014-06-07T01:44:38Z","html_url":"https://github.com/apache/spark/pull/895#discussion_r13517122","pull_request_url":"https://api.github.com/repos/apache/spark/pulls/895","_links":{"self":{"href":"https://api.github.com/repos/apache/spark/pulls/comments/13517122"},"html":{"href":"https://github.com/apache/spark/pull/895#discussion_r13517122"},"pull_request":{"href":"https://api.github.com/repos/apache/spark/pulls/895"}}},"pull_request":{"url":"https://api.github.com/repos/apache/spark/pulls/895","id":16399401,"html_url":"https://github.com/apache/spark/pull/895","diff_url":"https://github.com/apache/spark/pull/895.diff","patch_url":"https://github.com/apache/spark/pull/895.patch","issue_url":"https://api.github.com/repos/apache/spark/issues/895","number":895,"state":"open","title":"[SPARK-1940] Enabling rolling of executor logs, and automatic cleanup of old executor logs","user":{"login":"tdas","id":663212,"avatar_url":"https://avatars.githubusercontent.com/u/663212?","gravatar_id":"dd4cdf7510ba6e04e617f6c3a3fc62f7","url":"https://api.github.com/users/tdas","html_url":"https://github.com/tdas","followers_url":"https://api.github.com/users/tdas/followers","following_url":"https://api.github.com/users/tdas/following{/other_user}","gists_url":"https://api.github.com/users/tdas/gists{/gist_id}","starred_url":"https://api.github.com/users/tdas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tdas/subscriptions","organizations_url":"https://api.github.com/users/tdas/orgs","repos_url":"https://api.github.com/users/tdas/repos","events_url":"https://api.github.com/users/tdas/events{/privacy}","received_events_url":"https://api.github.com/users/tdas/received_events","type":"User","site_admin":false},"body":"Currently, in the default log4j configuration, all the executor logs get sent to the file <code>[executor-working-dir]/stderr</code>. This does not all log files to be rolled, so old logs cannot be removed.\r\n\r\nUsing log4j RollingFileAppender allows log4j logs to be rolled, but all the logs get sent to a different set of files, other than the files <code>stdout</code> and <code>stderr</code> . So the logs are not visible in the Spark web UI any more as Spark web UI only reads the files <code>stdout</code> and <code>stderr</code>. Furthermore, it still does not allow the stdout and stderr to be cleared periodically in case a large amount of stuff gets written to them (e.g. by explicit `println` inside map function).\r\n\r\nThis PR solves this by implementing a simple `RollingFileAppender` within Spark (disabled by default). When enabled (using configuration parameter `spark.executor.rollingLogs.enabled`), the logs can get rolled over either by time interval (set with `spark.executor.rollingLogs.interval`, set to daily by default), or by size of logs (set with  `spark.executor.rollingLogs.size`). Finally, old logs can be automatically deleted by specifying how many of the latest log files to keep (set with `spark.executor.rollingLogs.keepLastN`).  The web UI has also been modified to show the logs across the rolled-over files.\r\n\r\n\r\nYou can test this locally (without waiting a whole day) by setting  configuration `spark.executor.rollingLogs.enabled=true` and `spark.executor.rollingLogs.interval=minutely`. Continuously generate logs by running spark jobs and the generated logs files would look like this (`stderr` and `stdout` are the most current log file that are being written to). \r\n\r\n```\r\nstderr\r\nstderr--2014-05-27--14-37\r\nstderr--2014-05-27--14-47\r\nstderr--2014-05-27--15-05\r\nstdout\r\nstdout--2014-05-27--14-47\r\n```\r\n\r\nThe web ui should show logs across these files.","created_at":"2014-05-27T22:10:18Z","updated_at":"2014-06-07T01:44:38Z","closed_at":null,"merged_at":null,"merge_commit_sha":"30a7a78a7c9e246d9cd25e209f679ffd1bcbb94c","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/apache/spark/pulls/895/commits","review_comments_url":"https://api.github.com/repos/apache/spark/pulls/895/comments","review_comment_url":"https://api.github.com/repos/apache/spark/pulls/comments/{number}","comments_url":"https://api.github.com/repos/apache/spark/issues/895/comments","statuses_url":"https://api.github.com/repos/apache/spark/statuses/422440996e653ef5d77755909e64ada5d3160ee9","head":{"label":"tdas:rolling-logs","ref":"rolling-logs","sha":"422440996e653ef5d77755909e64ada5d3160ee9","user":{"login":"tdas","id":663212,"avatar_url":"https://avatars.githubusercontent.com/u/663212?","gravatar_id":"dd4cdf7510ba6e04e617f6c3a3fc62f7","url":"https://api.github.com/users/tdas","html_url":"https://github.com/tdas","followers_url":"https://api.github.com/users/tdas/followers","following_url":"https://api.github.com/users/tdas/following{/other_user}","gists_url":"https://api.github.com/users/tdas/gists{/gist_id}","starred_url":"https://api.github.com/users/tdas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tdas/subscriptions","organizations_url":"https://api.github.com/users/tdas/orgs","repos_url":"https://api.github.com/users/tdas/repos","events_url":"https://api.github.com/users/tdas/events{/privacy}","received_events_url":"https://api.github.com/users/tdas/received_events","type":"User","site_admin":false},"repo":{"id":17603343,"name":"spark","full_name":"tdas/spark","owner":{"login":"tdas","id":663212,"avatar_url":"https://avatars.githubusercontent.com/u/663212?","gravatar_id":"dd4cdf7510ba6e04e617f6c3a3fc62f7","url":"https://api.github.com/users/tdas","html_url":"https://github.com/tdas","followers_url":"https://api.github.com/users/tdas/followers","following_url":"https://api.github.com/users/tdas/following{/other_user}","gists_url":"https://api.github.com/users/tdas/gists{/gist_id}","starred_url":"https://api.github.com/users/tdas/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tdas/subscriptions","organizations_url":"https://api.github.com/users/tdas/orgs","repos_url":"https://api.github.com/users/tdas/repos","events_url":"https://api.github.com/users/tdas/events{/privacy}","received_events_url":"https://api.github.com/users/tdas/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/tdas/spark","description":"Mirror of Apache Spark","fork":true,"url":"https://api.github.com/repos/tdas/spark","forks_url":"https://api.github.com/repos/tdas/spark/forks","keys_url":"https://api.github.com/repos/tdas/spark/keys{/key_id}","collaborators_url":"https://api.github.com/repos/tdas/spark/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/tdas/spark/teams","hooks_url":"https://api.github.com/repos/tdas/spark/hooks","issue_events_url":"https://api.github.com/repos/tdas/spark/issues/events{/number}","events_url":"https://api.github.com/repos/tdas/spark/events","assignees_url":"https://api.github.com/repos/tdas/spark/assignees{/user}","branches_url":"https://api.github.com/repos/tdas/spark/branches{/branch}","tags_url":"https://api.github.com/repos/tdas/spark/tags","blobs_url":"https://api.github.com/repos/tdas/spark/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/tdas/spark/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/tdas/spark/git/refs{/sha}","trees_url":"https://api.github.com/repos/tdas/spark/git/trees{/sha}","statuses_url":"https://api.github.com/repos/tdas/spark/statuses/{sha}","languages_url":"https://api.github.com/repos/tdas/spark/languages","stargazers_url":"https://api.github.com/repos/tdas/spark/stargazers","contributors_url":"https://api.github.com/repos/tdas/spark/contributors","subscribers_url":"https://api.github.com/repos/tdas/spark/subscribers","subscription_url":"https://api.github.com/repos/tdas/spark/subscription","commits_url":"https://api.github.com/repos/tdas/spark/commits{/sha}","git_commits_url":"https://api.github.com/repos/tdas/spark/git/commits{/sha}","comments_url":"https://api.github.com/repos/tdas/spark/comments{/number}","issue_comment_url":"https://api.github.com/repos/tdas/spark/issues/comments/{number}","contents_url":"https://api.github.com/repos/tdas/spark/contents/{+path}","compare_url":"https://api.github.com/repos/tdas/spark/compare/{base}...{head}","merges_url":"https://api.github.com/repos/tdas/spark/merges","archive_url":"https://api.github.com/repos/tdas/spark/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/tdas/spark/downloads","issues_url":"https://api.github.com/repos/tdas/spark/issues{/number}","pulls_url":"https://api.github.com/repos/tdas/spark/pulls{/number}","milestones_url":"https://api.github.com/repos/tdas/spark/milestones{/number}","notifications_url":"https://api.github.com/repos/tdas/spark/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/tdas/spark/labels{/name}","releases_url":"https://api.github.com/repos/tdas/spark/releases{/id}","created_at":"2014-03-10T18:19:48Z","updated_at":"2014-06-05T04:18:00Z","pushed_at":"2014-06-05T04:18:00Z","git_url":"git://github.com/tdas/spark.git","ssh_url":"git@github.com:tdas/spark.git","clone_url":"https://github.com/tdas/spark.git","svn_url":"https://github.com/tdas/spark","homepage":null,"size":71130,"stargazers_count":0,"watchers_count":0,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"apache:master","ref":"master","sha":"abea2d4ff099036c67fc73136d0e61d0d0e22123","user":{"login":"apache","id":47359,"avatar_url":"https://avatars.githubusercontent.com/u/47359?","gravatar_id":"a676c0bf448fcd49f588249ead719b4c","url":"https://api.github.com/users/apache","html_url":"https://github.com/apache","followers_url":"https://api.github.com/users/apache/followers","following_url":"https://api.github.com/users/apache/following{/other_user}","gists_url":"https://api.github.com/users/apache/gists{/gist_id}","starred_url":"https://api.github.com/users/apache/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/apache/subscriptions","organizations_url":"https://api.github.com/users/apache/orgs","repos_url":"https://api.github.com/users/apache/repos","events_url":"https://api.github.com/users/apache/events{/privacy}","received_events_url":"https://api.github.com/users/apache/received_events","type":"Organization","site_admin":false},"repo":{"id":17165658,"name":"spark","full_name":"apache/spark","owner":{"login":"apache","id":47359,"avatar_url":"https://avatars.githubusercontent.com/u/47359?","gravatar_id":"a676c0bf448fcd49f588249ead719b4c","url":"https://api.github.com/users/apache","html_url":"https://github.com/apache","followers_url":"https://api.github.com/users/apache/followers","following_url":"https://api.github.com/users/apache/following{/other_user}","gists_url":"https://api.github.com/users/apache/gists{/gist_id}","starred_url":"https://api.github.com/users/apache/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/apache/subscriptions","organizations_url":"https://api.github.com/users/apache/orgs","repos_url":"https://api.github.com/users/apache/repos","events_url":"https://api.github.com/users/apache/events{/privacy}","received_events_url":"https://api.github.com/users/apache/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/apache/spark","description":"Mirror of Apache Spark","fork":false,"url":"https://api.github.com/repos/apache/spark","forks_url":"https://api.github.com/repos/apache/spark/forks","keys_url":"https://api.github.com/repos/apache/spark/keys{/key_id}","collaborators_url":"https://api.github.com/repos/apache/spark/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/apache/spark/teams","hooks_url":"https://api.github.com/repos/apache/spark/hooks","issue_events_url":"https://api.github.com/repos/apache/spark/issues/events{/number}","events_url":"https://api.github.com/repos/apache/spark/events","assignees_url":"https://api.github.com/repos/apache/spark/assignees{/user}","branches_url":"https://api.github.com/repos/apache/spark/branches{/branch}","tags_url":"https://api.github.com/repos/apache/spark/tags","blobs_url":"https://api.github.com/repos/apache/spark/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/apache/spark/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/apache/spark/git/refs{/sha}","trees_url":"https://api.github.com/repos/apache/spark/git/trees{/sha}","statuses_url":"https://api.github.com/repos/apache/spark/statuses/{sha}","languages_url":"https://api.github.com/repos/apache/spark/languages","stargazers_url":"https://api.github.com/repos/apache/spark/stargazers","contributors_url":"https://api.github.com/repos/apache/spark/contributors","subscribers_url":"https://api.github.com/repos/apache/spark/subscribers","subscription_url":"https://api.github.com/repos/apache/spark/subscription","commits_url":"https://api.github.com/repos/apache/spark/commits{/sha}","git_commits_url":"https://api.github.com/repos/apache/spark/git/commits{/sha}","comments_url":"https://api.github.com/repos/apache/spark/comments{/number}","issue_comment_url":"https://api.github.com/repos/apache/spark/issues/comments/{number}","contents_url":"https://api.github.com/repos/apache/spark/contents/{+path}","compare_url":"https://api.github.com/repos/apache/spark/compare/{base}...{head}","merges_url":"https://api.github.com/repos/apache/spark/merges","archive_url":"https://api.github.com/repos/apache/spark/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/apache/spark/downloads","issues_url":"https://api.github.com/repos/apache/spark/issues{/number}","pulls_url":"https://api.github.com/repos/apache/spark/pulls{/number}","milestones_url":"https://api.github.com/repos/apache/spark/milestones{/number}","notifications_url":"https://api.github.com/repos/apache/spark/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/apache/spark/labels{/name}","releases_url":"https://api.github.com/repos/apache/spark/releases{/id}","created_at":"2014-02-25T08:00:08Z","updated_at":"2014-06-07T00:54:31Z","pushed_at":"2014-06-06T18:45:54Z","git_url":"git://github.com/apache/spark.git","ssh_url":"git@github.com:apache/spark.git","clone_url":"https://github.com/apache/spark.git","svn_url":"https://github.com/apache/spark","homepage":null,"size":1519295,"stargazers_count":572,"watchers_count":572,"language":"Scala","has_issues":false,"has_downloads":true,"has_wiki":false,"forks_count":580,"mirror_url":"git://git.apache.org/spark.git","open_issues_count":178,"forks":580,"open_issues":178,"watchers":572,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/apache/spark/pulls/895"},"html":{"href":"https://github.com/apache/spark/pull/895"},"issue":{"href":"https://api.github.com/repos/apache/spark/issues/895"},"comments":{"href":"https://api.github.com/repos/apache/spark/issues/895/comments"},"review_comments":{"href":"https://api.github.com/repos/apache/spark/pulls/895/comments"},"review_comment":{"href":"https://api.github.com/repos/apache/spark/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/apache/spark/pulls/895/commits"},"statuses":{"href":"https://api.github.com/repos/apache/spark/statuses/422440996e653ef5d77755909e64ada5d3160ee9"}}}}