{ "info": { "author": "gian", "author_email": "gianmerlino@gmail.com", "bugtrack_url": null, "classifiers": [], "description": "# \"Rainer\"\n\nRainer is a configuration management library that is based around versioned key/value pairs called \"commits\". We\ncreated it after we noticed that we had a lot of services that each managed configuration in slightly different ways\n(with slightly different flaws!). It is composed of a set of APIs that can be used independently, or together as\npart of a powerful configuration management system. In addition to key/value access, it can also provide:\n\n- Full audit trail of historical commits for each key, including who, why, and when they were committed.\n- Ability to detect concurrent modifications and prevent users from clobbering each other.\n- Extensibility to handle a variety of storage backends and commit payload types.\n- Optional ZooKeeper-backed views and notifications, for immediate cluster-wide updates.\n- Optional HTTP API along with clients for the JVM, Python, and the command line.\n\nRainer is written in Scala, as are the examples on this page, but it should also be usable from other JVM\nlanguages (like Java).\n\n## Commits\n\nRainer is based around versioned key/value pairs called \"commits\". Commits have the attributes:\n\n- Key, which is a String.\n- Payload, which can be an arbitrary type. Think of this like a short document.\n- Version, which must increase by one with each successive commit.\n- Author, the entity that created the commit.\n- Comment, some free-form string describing the commit.\n- Mtime, the timestamp when the commit was created.\n\nEach key's commits are separate from every other key's commits, and are versioned independently. In particular,\nthere is no concept of a global database version.\n\nYour payload type must be deserializable, so you need to provide a KeyValueDeserialization for it. You can optionally\nprovide a KeyValueSerialization as well.\n\nIf you are not using a serializer, then when you create a new Commit, instead of providing the payload as an object,\nyou just provide some bytes that are deserializable using your KeyValueDeserialization. This mode is useful if you want\nto allow humans to edit the serialized documents directly, since it will preserve comments, whitespace, and so on.\n\n## Usage\n\nRainer offers three server-side components that can work together or separately:\n\n- CommitStorage (persistent journaled storage)\n- CommitKeeper (ZooKeeper-backed views and notifications)\n- RainerServlet (HTTP API for remote inspection and modification)\n\nIf you want to use all three together, the simplest way is using the \"Rainers\" builder, which creates all three and\nlinks them together:\n\n```scala\nimplicit val serialization = KeyValueSerialization.usingJackson[ValueType](objectMapper)\nimplicit val deserialization = KeyValueDeserialization.usingJackson[ValueType](objectMapper)\n\nval db = new MySQLDB(dbConfig) with DbCommitStorageMySQLMixin\nval rainers = Rainers.create[ValueType](\n curator,\n \"/path/in/zk\",\n new DbCommitStorage[ValueType](db, \"table_name\")\n)\n\ndb.start()\nrainers.start()\n\n// Do what you will with these:\nval myStorage: CommitStorage[ValueType] = rainers.storage\nval myKeeper: CommitKeeper[ValueType] = rainers.keeper\nval myServlet: RainerServlet[ValueType] = rainers.servlet\n\n// For example:\nmyStorage.save(Commit.fromValue(\n key = \"foo\",\n version = 1,\n payload = new ValueType,\n author = \"me\",\n comment = \"Creating foo\",\n mtime = DateTime.now\n))\n```\n\n## Components\n\n### CommitStorage: Persistent journaled storage\n\nThe CommitStorage trait represents a key/value store that retains every version of every commit. The main builtin\nimplementation is DbCommitStorage, which is backed by an RDBMS.\n\n```scala\nimplicit val deserialization = KeyValueDeserialization.usingJackson[ValueType](objectMapper)\n\nval db = new MySQLDB(dbConfig) with DbCommitStorageMySQLMixin\nval storage = new DbCommitStorage[ValueType](db, \"table_name\")\n\ndb.start()\nstorage.start()\n\n// Revert \"foo\" to previous version\nval foo = storage.get(\"foo\")\nval previousFoo = foo flatMap (x => storage.get(x.key, x.version - 1))\npreviousFoo foreach (x => storage.save(Commit.fromBytes(x.key, x.version + 2, x.payload, \"me\", \"Reverting foo\", DateTime.now))\n```\n\n### CommitKeeper: ZooKeeper views\n\nThe CommitKeeper class represents the most recent versions of each commit stored in ZooKeeper. It supports\non-demand access, live-updating views, and instant notifications. It does not keep a full history for each key; only\nCommitStorages do that.\n\n```scala\nimplicit val deserialization = KeyValueDeserialization.usingJackson[ValueType](objectMapper)\n\nval keeper = new CommitKeeper[ValueType](curator, \"/path/in/zk\")\n\n// On-demand access (synchronous ZK operations):\nval foo = keeper.get(\"foo\")\nval keys = keeper.keys\n\n// Create a mirror (async updates from ZK):\nval mirror: Var[Map[String, Commit[ValueType]]] = keeper.mirror()\n\n// Which, for example, you can use to update an AtomicReference:\nval ref = new AtomicReference[Map[String, Commit[ValueType]]]\nval c = mirror.changes.register(Witness(ref))\n\n// When you're no longer interested in updates, close the mirror:\nAwait.result(c.close())\n```\n\n### Using CommitKeeper with CommitStorage\n\nCombining a CommitStorage with a CommitKeeper is a common pattern used for managing configuration of a cluster of\nmachines. Combined setups can provide full-history journaled updates in an RDBMS, coupled with low latency\nnotifications through ZooKeeper. The idea is to do all of your saves through a keeperPublishing CommitStorage (which\nwill update ZooKeeper any time you save something) and to do your reads through a mirror provided by a CommitKeeper. To\nprevent inopportune crashes or out of band ZooKeeper modifications from causing ZooKeeper to become out of sync, an\nautoPublisher can periodically detect and push any unpushed updates from your storage.\n\nThe \"Rainers\" builder sets all of this up for you, but you can also do it manually with something like:\n\n```scala\n// During setup, link your underlying storage and keeper together.\n// Afterwards, use \"storage\", not \"underlyingStorage\" for your own operations.\nval keeper = new CommitKeeper[ValueType](curator, \"/path/in/zk\")\nval storage = CommitStorage.keeperPublishing(underlyingStorage, keeper)\nval autoPublisher = keeper.autoPublisher(underlyingStorage, period, periodFuzz)\nautoPublisher.start()\n\n// When your application exits, shut down your autoPublisher.\nAwait.result(autoPublisher.close())\n```\n\nWhen using a linked CommitStorage and CommitKeeper, make sure to send writes through the CommitStorage only. This is\nbecause the CommitStorage is treated as the system of record.\n\n```scala\n// You can get a mirror from your keeper, which will now reflect the most recent commits from your storage.\nval mirror: Var[Map[String, Commit[ValueType]]] = keeper.mirror()\n\n// Which, for example, you can use to update an AtomicReference:\nval ref = new AtomicReference[Map[String, Commit[ValueType]]]\nval c = mirror.changes.register(Witness(ref))\n\n// When you save to the storage, ZooKeeper and the mirror will be updated automatically. (Don't save using the keeper!)\nstorage.save(someCommit)\n```\n\n### Using CommitStorage without CommitKeeper\n\nYou can use a CommitStorage by itself. The only thing you lose are the ZooKeeper views, which means you can't set up\nmirrors. In this case you'll either need to access the underlying storage on-demand, or set up a cache yourself that\nrefreshes periodically.\n\n### Using CommitKeeper without CommitStorage\n\nYou can use CommitKeeper by itself, too, without any backing journaled storage. You won't be able to access old\nversions of any commits. Just call ```keeper.save(commit)``` to publish new commits, and don't use the autoPublisher.\nThe other CommitKeeper features will work normally, including gets and mirrors.\n\n### HTTP server\n\nYou can use the RainerServlet to gain a nice HTTP API to your CommitStorage. It does not use, or require, a\nCommitKeeper. The API can be used like this with any HTTP client:\n\n```\n$ curl \\\n -s \\\n -XPOST \\\n -H'Content-Type: application/octet-stream' \\\n -H'X-Rainer-Author: gian' \\\n -H'X-Rainer-Comment: rofl' \\\n --data-binary '{\"hey\":\"what\"}' \\\n http://localhost:8080/diary/foo/1\n{\"author\":\"gian\",\"comment\":\"rofl\",\"key\":\"foo\",\"mtime\":\"2014-02-11T14:01:28.839-08:00\",\"version\":1}\n\n$ curl -s http://localhost:8080/diary/foo\n{\"hey\":\"what\"}\n\n$ curl -s http://localhost:8080/diary/foo/1\n{\"hey\":\"what\"}\n\n$ curl -s http://localhost:8080/diary/foo/1/meta\n{\"author\":\"gian\",\"key\":\"foo\",\"version\":1,\"mtime\":\"2014-02-11T14:01:28.839-08:00\",\"comment\":\"rofl\"}\n```\n\n## Clients\n\nIf you have a RainerServlet running, there are a few pre-built clients that can be used to inspect and modify\nconfigurations remotely.\n\n### JVM HTTP client\n\nYou can use HttpCommitStorage to access a RainerServlet running on another machine:\n\n```scala\nval uri = new URI(\"http://localhost:8080/diary\")\nval client = ClientBuilder()\n .name(uri.toString)\n .codec(Http())\n .group(Group.fromVarAddr(InetResolver.bind(uri.getAuthority)))\n .hostConnectionLimit(2)\n .build()\nval storage = new HttpCommitStorage[ValueType](client, uri)\n\n// Use it like any other CommitStorage:\nval commit = storage.get(\"hey\")\n```\n\n### Python HTTP client\n\nFrom Python, you can use \"pyrainer\". You can install it with ```pip install pyrainer``` and use it like this:\n\n```python\nimport pyrainer.http\n\nclient = pyrainer.http.RainerClient(\"http://localhost:8080/diary\")\n\ncommits = client.list_full()\nfor key in sorted(commits.keys()):\n version = commits[key].version\n print \"%s [current version = %s]\" % (key, str(version))\n\nhey = client.get_commit(\"hey\")\nprint \"Commit version %s = %s\" % (hey.meta[\"version\"], hey.value)\n\nclient.post_commit({\"key\": \"hey\", \"version\": 2, \"author\": \"sue\", \"comment\": \"rofl\"}, \"new value\")\n```\n\n### Command-line client\n\nIf you have pyrainer installed (```pip install pyrainer```), you can use the command line tool interactively or in\nshell scripts.\n\n```\n$ python -m pyrainer.rainer --url http://localhost:8080/diary list\nfoo 1 http://localhost:8080/diary/foo/1\nhey 6 http://localhost:8080/diary/hey/6\n\n$ python -m pyrainer.rainer --url http://localhost:8080/diary show hey\n{\"hey\":\"whatsit\"}\n\n$ python -m pyrainer.rainer --url http://localhost:8080/diary edit hey\n[...a wild $EDITOR appears!]\n```\n\nYou can also create a wrapper for your service that makes invocation simpler. An executable script like this should\ndo it:\n\n```bash\n#!/bin/sh -e\nexec python -m pyrainer.rainer --url \"http://example.com/foo\" \"$@\"\n```", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/metamx/rainer", "keywords": null, "license": "LICENSE", "maintainer": null, "maintainer_email": null, "name": "pyrainer", "package_url": "https://pypi.org/project/pyrainer/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/pyrainer/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/metamx/rainer" }, "release_url": "https://pypi.org/project/pyrainer/0.22/", "requires_dist": null, "requires_python": null, "summary": "Python client for Rainer", "version": "0.22" }, "last_serial": 1648549, "releases": { "0.14": [ { "comment_text": "", "digests": { "md5": "f16c4423149feade5d88f5d94268c5bd", "sha256": "99ac8a511473a87cb2bb447bfceb96d8e325e841ac1562a7c6b95f4a2bf0681c" }, "downloads": -1, "filename": "pyrainer-0.14.tar.gz", "has_sig": false, "md5_digest": "f16c4423149feade5d88f5d94268c5bd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8172, "upload_time": "2014-03-19T23:10:26", "url": "https://files.pythonhosted.org/packages/57/af/8b953ccf2be18c95017ee9153de503aa90cc718e5da4ab97aeb4ab81817a/pyrainer-0.14.tar.gz" } ], "0.15": [ { "comment_text": "", "digests": { "md5": "0b88e845cd4588a2b3ca7c8572b8efd3", "sha256": "1af95f24ae34cf45c9abd6c9325f4f4d1084d7738c4ccbd0e3f62f9b9bbf7b58" }, "downloads": -1, "filename": "pyrainer-0.15.tar.gz", "has_sig": false, "md5_digest": "0b88e845cd4588a2b3ca7c8572b8efd3", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8257, "upload_time": "2014-03-20T20:55:34", "url": "https://files.pythonhosted.org/packages/b9/1a/72b199bc47f9677b7189443d4eb9fb2b960ea93d953c363aada2b17d695d/pyrainer-0.15.tar.gz" } ], "0.16": [ { "comment_text": "", "digests": { "md5": "85bd541ceef5520935265d4d259537ab", "sha256": "be06003281f7bf73885323a3976be16027dc26c6e1f59dd0017176f7b57077bd" }, "downloads": -1, "filename": "pyrainer-0.16.tar.gz", "has_sig": false, "md5_digest": "85bd541ceef5520935265d4d259537ab", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8505, "upload_time": "2014-04-14T20:42:32", "url": "https://files.pythonhosted.org/packages/81/18/be34d2f9d6aaed60c4e7e6dddf6c2caf62192ce45a4d6aee0731ea6a855b/pyrainer-0.16.tar.gz" } ], "0.17": [ { "comment_text": "", "digests": { "md5": "e7b38e0f90bbbf11a9eb576763b90b65", "sha256": "e72d73f29104b08f46058c8581f60695530191fde00eea650cadf54e810e03f0" }, "downloads": -1, "filename": "pyrainer-0.17.tar.gz", "has_sig": false, "md5_digest": "e7b38e0f90bbbf11a9eb576763b90b65", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8510, "upload_time": "2014-05-20T16:52:59", "url": "https://files.pythonhosted.org/packages/de/18/b7117a483f0b76c9922ef7b222607c99c2d2aaf888880bf65fece215f218/pyrainer-0.17.tar.gz" } ], "0.18": [ { "comment_text": "", "digests": { "md5": "5c7379e862ae10e0a2a8e1c21535c466", "sha256": "dd2f2b16d1da55c18312f83e958be12100a9cfb76b2cf775e78c7ac6d2e1c234" }, "downloads": -1, "filename": "pyrainer-0.18.tar.gz", "has_sig": false, "md5_digest": "5c7379e862ae10e0a2a8e1c21535c466", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8538, "upload_time": "2014-05-30T18:25:55", "url": "https://files.pythonhosted.org/packages/35/91/974789d0b9aaddac91de006507f9116289ebc9b6581067ee79c60f29f759/pyrainer-0.18.tar.gz" } ], "0.19": [ { "comment_text": "", "digests": { "md5": "ea747a2d8a9b59d41fac415942d0c01c", "sha256": "cfdb9870bd92a245318de60587978edd386d90db3c303ae4b800abd695370546" }, "downloads": -1, "filename": "pyrainer-0.19.tar.gz", "has_sig": false, "md5_digest": "ea747a2d8a9b59d41fac415942d0c01c", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8563, "upload_time": "2014-06-06T04:47:15", "url": "https://files.pythonhosted.org/packages/c8/4e/9126aa8aaa40d7d296f0d3707038aedb10abf649005479112a81cc6e0c45/pyrainer-0.19.tar.gz" } ], "0.20": [ { "comment_text": "", "digests": { "md5": "253d26c2279f97909da542a01bd006cd", "sha256": "5adcd55076d38dbb6bcc09fb202e9f8fd0ed7e6dceb44c9f18a19fa53c099864" }, "downloads": -1, "filename": "pyrainer-0.20.tar.gz", "has_sig": false, "md5_digest": "253d26c2279f97909da542a01bd006cd", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 8776, "upload_time": "2014-07-29T00:17:17", "url": "https://files.pythonhosted.org/packages/7f/c2/84f5299ae87c2982ec90734d20e416f34d3216b11af01065035916a3120d/pyrainer-0.20.tar.gz" } ], "0.21": [ { "comment_text": "", "digests": { "md5": "b551dc9f4c1b49a07403204d9f0ce4a5", "sha256": "e504dd26797b23daa9ce9d992c4ff5087a0ecd8263e115e83ff5bce809cbb871" }, "downloads": -1, "filename": "pyrainer-0.21.tar.gz", "has_sig": false, "md5_digest": "b551dc9f4c1b49a07403204d9f0ce4a5", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 13648, "upload_time": "2014-12-19T17:57:58", "url": "https://files.pythonhosted.org/packages/19/47/7e109bbd93e8a7a016da877c4f64729919e0c92fbb904fe6f5826ee9b24a/pyrainer-0.21.tar.gz" } ], "0.22": [ { "comment_text": "", "digests": { "md5": "a5d7c2358029e4c4a9216a47ee44a62e", "sha256": "729403550272d70617514d83fb0a62d7f778b6ad1935ff403cbba48cdc8eaa95" }, "downloads": -1, "filename": "pyrainer-0.22.tar.gz", "has_sig": false, "md5_digest": "a5d7c2358029e4c4a9216a47ee44a62e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 13682, "upload_time": "2015-01-15T19:54:55", "url": "https://files.pythonhosted.org/packages/c7/67/28940bd3b2ca96095a00aafecee5bdb58b0427fb619e7b605946844b2b5f/pyrainer-0.22.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "a5d7c2358029e4c4a9216a47ee44a62e", "sha256": "729403550272d70617514d83fb0a62d7f778b6ad1935ff403cbba48cdc8eaa95" }, "downloads": -1, "filename": "pyrainer-0.22.tar.gz", "has_sig": false, "md5_digest": "a5d7c2358029e4c4a9216a47ee44a62e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 13682, "upload_time": "2015-01-15T19:54:55", "url": "https://files.pythonhosted.org/packages/c7/67/28940bd3b2ca96095a00aafecee5bdb58b0427fb619e7b605946844b2b5f/pyrainer-0.22.tar.gz" } ] }