{"action":"created","comment":{"url":"https://api.github.com/repos/4teamwork/opengever.core/pulls/comments/277725042","pull_request_review_id":229518345,"id":277725042,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI3NzcyNTA0Mg==","diff_hunk":"@@ -0,0 +1,119 @@\n+from datetime import datetime\n+from datetime import timedelta\n+from opengever.base.sentry import log_msg_to_sentry\n+from opengever.nightlyjobs.interfaces import INightlyJobProvider\n+from opengever.nightlyjobs.interfaces import INightlyJobsSettings\n+from plone import api\n+from zope.component import getAdapters\n+from zope.globalrequest import getRequest\n+import transaction\n+\n+\n+class TimeWindowExceeded(Exception):\n+\n+    message = \"Time window exceeded. Window: {:%H:%M}-{:%H:%M}. \"\\\n+              \"Current time: {:%H:%M}\"\n+\n+    def __init__(self, start, end):\n+        self.current_time = datetime.now().time()\n+        self.start = start\n+        self.end = end\n+        message = self.message.format(start, end, self.current_time)\n+        super(TimeWindowExceeded, self).__init__(message)\n+\n+\n+class SystemLoadCritical(Exception):\n+    pass\n+\n+\n+class NightlyJobRunner(object):\n+    \"\"\" This class is used to execute nightly jobs.\n+    It will try to execute all jobs provided by the registered\n+    nightly job providers (named multiadapters of INightlyJobProvider).\n+    It will stop execution when not in the defined timeframe or when\n+    the system load is too high.\n+    \"\"\"\n+\n+    def __init__(self):\n+        # retrieve window start and end times\n+        self.window_start = api.portal.get_registry_record(\n+            'start_time', interface=INightlyJobsSettings)\n+        self.window_end = api.portal.get_registry_record(\n+            'end_time', interface=INightlyJobsSettings)\n+        self.window_length = self._positive_timedelta(\n+            self.window_start, self.window_end)\n+\n+        # retrieve all providers\n+        self.job_providers = {name: provider for name, provider\n+                              in getAdapters([api.portal.get(), getRequest()],\n+                                             INightlyJobProvider)}\n+\n+    @staticmethod\n+    def _positive_timedelta(time1, time2):\n+        \"\"\" Returns the positive timedelta between two time objects (time2-time1).\n+        This is necessary to correctly handle timedeltas around midnight.\n+        \"\"\"\n+        datetime1 = datetime(1900, 1, 1, time1.hour, time1.minute, time1.second)\n+        datetime2 = datetime(1900, 1, 1, time2.hour, time2.minute, time2.second)\n+        if datetime2 < datetime1:\n+            datetime2 += timedelta(1)\n+        return datetime2 - datetime1\n+\n+    def get_jobs(self):\n+        for job_provider in self.job_providers.values():\n+            for job in job_provider:\n+                yield job\n+\n+    def execute_pending_jobs(self):\n+        for job in self.get_jobs():\n+            try:\n+                self.interrupt_if_necessary()\n+                provider = self.job_providers.get(job.provider_name)\n+                provider.run_job(job, self.interrupt_if_necessary)\n+            except (TimeWindowExceeded, SystemLoadCritical) as exc:\n+                transaction.abort()\n+                message = self.format_early_abort_message(exc)\n+                self.log_to_sentry(message)\n+                return exc\n+            transaction.commit()\n+\n+    def interrupt_if_necessary(self):\n+        if not self.check_in_time_window():\n+            raise TimeWindowExceeded(self.window_start, self.window_end)\n+        if self.check_system_overloaded():\n+            raise SystemLoadCritical()\n+\n+    def check_in_time_window(self):\n+        current_time = datetime.now().time()\n+        current_timedelta = self._positive_timedelta(self.window_start, current_time)\n+        return current_timedelta < self.window_length\n+\n+    def check_system_overloaded(self):\n+        return False\n+\n+    def format_early_abort_message(self, exc):\n+        info = \"\\n\".join(\"{} executed {} out of {} jobs\".format(\n+            provider.provider_name, provider.njobs_executed, provider.njobs)\n+            for provider in self.job_providers.values())\n+        return \"{}\\n{}\".format(repr(exc), info)\n+\n+    def log_to_sentry(self, message):\n+        log_msg_to_sentry(message, request=getRequest())\n+\n+    @property\n+    def njobs_remaining(self):\n+        if not self.job_providers:\n+            return 0\n+        return sum(len(provider) for provider in self.job_providers.values())\n+\n+    @property\n+    def njobs_total(self):","path":"opengever/nightlyjobs/runner.py","position":131,"original_position":110,"commit_id":"103b01c3b91484ff136d645988ad18e62092b3ff","original_commit_id":"a5e4fec0648f91b81eb2fbfe283dca7390a9f14d","user":{"login":"lukasgraf","id":405124,"node_id":"MDQ6VXNlcjQwNTEyNA==","avatar_url":"https://avatars2.githubusercontent.com/u/405124?v=4","gravatar_id":"","url":"https://api.github.com/users/lukasgraf","html_url":"https://github.com/lukasgraf","followers_url":"https://api.github.com/users/lukasgraf/followers","following_url":"https://api.github.com/users/lukasgraf/following{/other_user}","gists_url":"https://api.github.com/users/lukasgraf/gists{/gist_id}","starred_url":"https://api.github.com/users/lukasgraf/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/lukasgraf/subscriptions","organizations_url":"https://api.github.com/users/lukasgraf/orgs","repos_url":"https://api.github.com/users/lukasgraf/repos","events_url":"https://api.github.com/users/lukasgraf/events{/privacy}","received_events_url":"https://api.github.com/users/lukasgraf/received_events","type":"User","site_admin":false},"body":"I would lean towards tracking this state on the runner instead requiring the providers to do so.\r\n\r\nSo, instead of separately asking the providers to list their current job count and keep track of their total as well as executed job count, I would simply require them to provide a `__len__` (current job count).\r\n\r\nThe runner can then make sure to query the job providers once in the beginning for their `__len__` keep track of it itself (same for number of execute jobs - the runner doesn't need the provider's help to accomplish that).","created_at":"2019-04-23T15:01:42Z","updated_at":"2019-04-23T15:31:14Z","html_url":"https://github.com/4teamwork/opengever.core/pull/5566#discussion_r277725042","pull_request_url":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/4teamwork/opengever.core/pulls/comments/277725042"},"html":{"href":"https://github.com/4teamwork/opengever.core/pull/5566#discussion_r277725042"},"pull_request":{"href":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566"}}},"pull_request":{"url":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566","id":270883332,"node_id":"MDExOlB1bGxSZXF1ZXN0MjcwODgzMzMy","html_url":"https://github.com/4teamwork/opengever.core/pull/5566","diff_url":"https://github.com/4teamwork/opengever.core/pull/5566.diff","patch_url":"https://github.com/4teamwork/opengever.core/pull/5566.patch","issue_url":"https://api.github.com/repos/4teamwork/opengever.core/issues/5566","number":5566,"state":"open","locked":false,"title":"Add nightly job runner","user":{"login":"njohner","id":7374243,"node_id":"MDQ6VXNlcjczNzQyNDM=","avatar_url":"https://avatars3.githubusercontent.com/u/7374243?v=4","gravatar_id":"","url":"https://api.github.com/users/njohner","html_url":"https://github.com/njohner","followers_url":"https://api.github.com/users/njohner/followers","following_url":"https://api.github.com/users/njohner/following{/other_user}","gists_url":"https://api.github.com/users/njohner/gists{/gist_id}","starred_url":"https://api.github.com/users/njohner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/njohner/subscriptions","organizations_url":"https://api.github.com/users/njohner/orgs","repos_url":"https://api.github.com/users/njohner/repos","events_url":"https://api.github.com/users/njohner/events{/privacy}","received_events_url":"https://api.github.com/users/njohner/received_events","type":"User","site_admin":false},"body":"We add a `NightlyJobRunner` class to execute nightly jobs (such as resolving dossiers, generating archive PDFs ...). The construction is as follows:\r\n* `NightlyJobRunner`: It looks for all `INightlyJobProvider`s and executes the jobs provided by them. It will abort if outside of a given time window (defined in the registry `INightlyJobsSettings`) or if the system load is too high.\r\n* `BaseJobProvider`: \r\n    * Base implementation of a job provider, which is meant to be subclassed (subclasses will have to be registered as implementing `INightlyJobProvider`).\r\n    * Takes care of iteration and bookkeeping (how many jobs were executed, how many are left, etc.). \r\n    * subclasses have to implement the  \r\n        * `execute_job` method, which will do the actual work \r\n        * `initialize` method which needs to provide a generator (or any iterable) of the jobs as well as the number of jobs to be executed. \r\n    * The class is designed to be flexible and it should be fairly easy to subclass it for a provider managing its jobs with a queue (for example remove job from queue in the `post_job_run` method)\r\n* `NightlyJob` is the baseclass for the nightly jobs themselves. This basically does nothing except indicate that subclasses have to define the `provider_name` class attribute. This is used by the runner to know which provider to use to execute a particular job.\r\n* `run_nightly_jobs` is the `zopectl.command` meant to be run by a cronjob.\r\n\r\nAn example implementation of a nightly job and a job provider can be found in the tests (`DocumentTitleModifierJobProvider` and `ModifyTitleJob`)\r\n\r\nFor system load monitoring we add `psutil` as dependency. For now monitoring is kept very simple. We could make the load limits registry settings?\r\n\r\nI tested the whole implementation by registering a job provider and running jobs with the `run_nightly_jobs` command. The only thing that is not tested is the logging to sentry, as I don't know how to test it.\r\n\r\nAlso note that I had to overwrite an adapter for the registry to allow `Time` fields in the registry. I did not make a `registry` module for that, but maybe we should?\r\n\r\nresoves #5554 ","created_at":"2019-04-16T12:16:43Z","updated_at":"2019-04-23T15:31:14Z","closed_at":null,"merged_at":null,"merge_commit_sha":null,"assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566/commits","review_comments_url":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566/comments","review_comment_url":"https://api.github.com/repos/4teamwork/opengever.core/pulls/comments{/number}","comments_url":"https://api.github.com/repos/4teamwork/opengever.core/issues/5566/comments","statuses_url":"https://api.github.com/repos/4teamwork/opengever.core/statuses/103b01c3b91484ff136d645988ad18e62092b3ff","head":{"label":"4teamwork:nj_nightly_runner","ref":"nj_nightly_runner","sha":"103b01c3b91484ff136d645988ad18e62092b3ff","user":{"login":"4teamwork","id":681821,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4MTgyMQ==","avatar_url":"https://avatars2.githubusercontent.com/u/681821?v=4","gravatar_id":"","url":"https://api.github.com/users/4teamwork","html_url":"https://github.com/4teamwork","followers_url":"https://api.github.com/users/4teamwork/followers","following_url":"https://api.github.com/users/4teamwork/following{/other_user}","gists_url":"https://api.github.com/users/4teamwork/gists{/gist_id}","starred_url":"https://api.github.com/users/4teamwork/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/4teamwork/subscriptions","organizations_url":"https://api.github.com/users/4teamwork/orgs","repos_url":"https://api.github.com/users/4teamwork/repos","events_url":"https://api.github.com/users/4teamwork/events{/privacy}","received_events_url":"https://api.github.com/users/4teamwork/received_events","type":"Organization","site_admin":false},"repo":{"id":9788097,"node_id":"MDEwOlJlcG9zaXRvcnk5Nzg4MDk3","name":"opengever.core","full_name":"4teamwork/opengever.core","private":false,"owner":{"login":"4teamwork","id":681821,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4MTgyMQ==","avatar_url":"https://avatars2.githubusercontent.com/u/681821?v=4","gravatar_id":"","url":"https://api.github.com/users/4teamwork","html_url":"https://github.com/4teamwork","followers_url":"https://api.github.com/users/4teamwork/followers","following_url":"https://api.github.com/users/4teamwork/following{/other_user}","gists_url":"https://api.github.com/users/4teamwork/gists{/gist_id}","starred_url":"https://api.github.com/users/4teamwork/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/4teamwork/subscriptions","organizations_url":"https://api.github.com/users/4teamwork/orgs","repos_url":"https://api.github.com/users/4teamwork/repos","events_url":"https://api.github.com/users/4teamwork/events{/privacy}","received_events_url":"https://api.github.com/users/4teamwork/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/4teamwork/opengever.core","description":"OneGov GEVER core package","fork":false,"url":"https://api.github.com/repos/4teamwork/opengever.core","forks_url":"https://api.github.com/repos/4teamwork/opengever.core/forks","keys_url":"https://api.github.com/repos/4teamwork/opengever.core/keys{/key_id}","collaborators_url":"https://api.github.com/repos/4teamwork/opengever.core/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/4teamwork/opengever.core/teams","hooks_url":"https://api.github.com/repos/4teamwork/opengever.core/hooks","issue_events_url":"https://api.github.com/repos/4teamwork/opengever.core/issues/events{/number}","events_url":"https://api.github.com/repos/4teamwork/opengever.core/events","assignees_url":"https://api.github.com/repos/4teamwork/opengever.core/assignees{/user}","branches_url":"https://api.github.com/repos/4teamwork/opengever.core/branches{/branch}","tags_url":"https://api.github.com/repos/4teamwork/opengever.core/tags","blobs_url":"https://api.github.com/repos/4teamwork/opengever.core/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/4teamwork/opengever.core/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/4teamwork/opengever.core/git/refs{/sha}","trees_url":"https://api.github.com/repos/4teamwork/opengever.core/git/trees{/sha}","statuses_url":"https://api.github.com/repos/4teamwork/opengever.core/statuses/{sha}","languages_url":"https://api.github.com/repos/4teamwork/opengever.core/languages","stargazers_url":"https://api.github.com/repos/4teamwork/opengever.core/stargazers","contributors_url":"https://api.github.com/repos/4teamwork/opengever.core/contributors","subscribers_url":"https://api.github.com/repos/4teamwork/opengever.core/subscribers","subscription_url":"https://api.github.com/repos/4teamwork/opengever.core/subscription","commits_url":"https://api.github.com/repos/4teamwork/opengever.core/commits{/sha}","git_commits_url":"https://api.github.com/repos/4teamwork/opengever.core/git/commits{/sha}","comments_url":"https://api.github.com/repos/4teamwork/opengever.core/comments{/number}","issue_comment_url":"https://api.github.com/repos/4teamwork/opengever.core/issues/comments{/number}","contents_url":"https://api.github.com/repos/4teamwork/opengever.core/contents/{+path}","compare_url":"https://api.github.com/repos/4teamwork/opengever.core/compare/{base}...{head}","merges_url":"https://api.github.com/repos/4teamwork/opengever.core/merges","archive_url":"https://api.github.com/repos/4teamwork/opengever.core/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/4teamwork/opengever.core/downloads","issues_url":"https://api.github.com/repos/4teamwork/opengever.core/issues{/number}","pulls_url":"https://api.github.com/repos/4teamwork/opengever.core/pulls{/number}","milestones_url":"https://api.github.com/repos/4teamwork/opengever.core/milestones{/number}","notifications_url":"https://api.github.com/repos/4teamwork/opengever.core/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/4teamwork/opengever.core/labels{/name}","releases_url":"https://api.github.com/repos/4teamwork/opengever.core/releases{/id}","deployments_url":"https://api.github.com/repos/4teamwork/opengever.core/deployments","created_at":"2013-05-01T08:28:16Z","updated_at":"2019-04-23T12:56:32Z","pushed_at":"2019-04-23T14:40:10Z","git_url":"git://github.com/4teamwork/opengever.core.git","ssh_url":"git@github.com:4teamwork/opengever.core.git","clone_url":"https://github.com/4teamwork/opengever.core.git","svn_url":"https://github.com/4teamwork/opengever.core","homepage":"","size":108460,"stargazers_count":5,"watchers_count":5,"language":"Python","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":228,"license":null,"forks":4,"open_issues":228,"watchers":5,"default_branch":"master"}},"base":{"label":"4teamwork:master","ref":"master","sha":"3233afc74753f295b24939b41159b366d27d7dd6","user":{"login":"4teamwork","id":681821,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4MTgyMQ==","avatar_url":"https://avatars2.githubusercontent.com/u/681821?v=4","gravatar_id":"","url":"https://api.github.com/users/4teamwork","html_url":"https://github.com/4teamwork","followers_url":"https://api.github.com/users/4teamwork/followers","following_url":"https://api.github.com/users/4teamwork/following{/other_user}","gists_url":"https://api.github.com/users/4teamwork/gists{/gist_id}","starred_url":"https://api.github.com/users/4teamwork/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/4teamwork/subscriptions","organizations_url":"https://api.github.com/users/4teamwork/orgs","repos_url":"https://api.github.com/users/4teamwork/repos","events_url":"https://api.github.com/users/4teamwork/events{/privacy}","received_events_url":"https://api.github.com/users/4teamwork/received_events","type":"Organization","site_admin":false},"repo":{"id":9788097,"node_id":"MDEwOlJlcG9zaXRvcnk5Nzg4MDk3","name":"opengever.core","full_name":"4teamwork/opengever.core","private":false,"owner":{"login":"4teamwork","id":681821,"node_id":"MDEyOk9yZ2FuaXphdGlvbjY4MTgyMQ==","avatar_url":"https://avatars2.githubusercontent.com/u/681821?v=4","gravatar_id":"","url":"https://api.github.com/users/4teamwork","html_url":"https://github.com/4teamwork","followers_url":"https://api.github.com/users/4teamwork/followers","following_url":"https://api.github.com/users/4teamwork/following{/other_user}","gists_url":"https://api.github.com/users/4teamwork/gists{/gist_id}","starred_url":"https://api.github.com/users/4teamwork/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/4teamwork/subscriptions","organizations_url":"https://api.github.com/users/4teamwork/orgs","repos_url":"https://api.github.com/users/4teamwork/repos","events_url":"https://api.github.com/users/4teamwork/events{/privacy}","received_events_url":"https://api.github.com/users/4teamwork/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/4teamwork/opengever.core","description":"OneGov GEVER core package","fork":false,"url":"https://api.github.com/repos/4teamwork/opengever.core","forks_url":"https://api.github.com/repos/4teamwork/opengever.core/forks","keys_url":"https://api.github.com/repos/4teamwork/opengever.core/keys{/key_id}","collaborators_url":"https://api.github.com/repos/4teamwork/opengever.core/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/4teamwork/opengever.core/teams","hooks_url":"https://api.github.com/repos/4teamwork/opengever.core/hooks","issue_events_url":"https://api.github.com/repos/4teamwork/opengever.core/issues/events{/number}","events_url":"https://api.github.com/repos/4teamwork/opengever.core/events","assignees_url":"https://api.github.com/repos/4teamwork/opengever.core/assignees{/user}","branches_url":"https://api.github.com/repos/4teamwork/opengever.core/branches{/branch}","tags_url":"https://api.github.com/repos/4teamwork/opengever.core/tags","blobs_url":"https://api.github.com/repos/4teamwork/opengever.core/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/4teamwork/opengever.core/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/4teamwork/opengever.core/git/refs{/sha}","trees_url":"https://api.github.com/repos/4teamwork/opengever.core/git/trees{/sha}","statuses_url":"https://api.github.com/repos/4teamwork/opengever.core/statuses/{sha}","languages_url":"https://api.github.com/repos/4teamwork/opengever.core/languages","stargazers_url":"https://api.github.com/repos/4teamwork/opengever.core/stargazers","contributors_url":"https://api.github.com/repos/4teamwork/opengever.core/contributors","subscribers_url":"https://api.github.com/repos/4teamwork/opengever.core/subscribers","subscription_url":"https://api.github.com/repos/4teamwork/opengever.core/subscription","commits_url":"https://api.github.com/repos/4teamwork/opengever.core/commits{/sha}","git_commits_url":"https://api.github.com/repos/4teamwork/opengever.core/git/commits{/sha}","comments_url":"https://api.github.com/repos/4teamwork/opengever.core/comments{/number}","issue_comment_url":"https://api.github.com/repos/4teamwork/opengever.core/issues/comments{/number}","contents_url":"https://api.github.com/repos/4teamwork/opengever.core/contents/{+path}","compare_url":"https://api.github.com/repos/4teamwork/opengever.core/compare/{base}...{head}","merges_url":"https://api.github.com/repos/4teamwork/opengever.core/merges","archive_url":"https://api.github.com/repos/4teamwork/opengever.core/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/4teamwork/opengever.core/downloads","issues_url":"https://api.github.com/repos/4teamwork/opengever.core/issues{/number}","pulls_url":"https://api.github.com/repos/4teamwork/opengever.core/pulls{/number}","milestones_url":"https://api.github.com/repos/4teamwork/opengever.core/milestones{/number}","notifications_url":"https://api.github.com/repos/4teamwork/opengever.core/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/4teamwork/opengever.core/labels{/name}","releases_url":"https://api.github.com/repos/4teamwork/opengever.core/releases{/id}","deployments_url":"https://api.github.com/repos/4teamwork/opengever.core/deployments","created_at":"2013-05-01T08:28:16Z","updated_at":"2019-04-23T12:56:32Z","pushed_at":"2019-04-23T14:40:10Z","git_url":"git://github.com/4teamwork/opengever.core.git","ssh_url":"git@github.com:4teamwork/opengever.core.git","clone_url":"https://github.com/4teamwork/opengever.core.git","svn_url":"https://github.com/4teamwork/opengever.core","homepage":"","size":108460,"stargazers_count":5,"watchers_count":5,"language":"Python","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":4,"mirror_url":null,"archived":false,"disabled":false,"open_issues_count":228,"license":null,"forks":4,"open_issues":228,"watchers":5,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566"},"html":{"href":"https://github.com/4teamwork/opengever.core/pull/5566"},"issue":{"href":"https://api.github.com/repos/4teamwork/opengever.core/issues/5566"},"comments":{"href":"https://api.github.com/repos/4teamwork/opengever.core/issues/5566/comments"},"review_comments":{"href":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566/comments"},"review_comment":{"href":"https://api.github.com/repos/4teamwork/opengever.core/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/4teamwork/opengever.core/pulls/5566/commits"},"statuses":{"href":"https://api.github.com/repos/4teamwork/opengever.core/statuses/103b01c3b91484ff136d645988ad18e62092b3ff"}},"author_association":"MEMBER"}}
{"actor":{"display_login":"lukasgraf"}}