Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add subscriptions #3390

Merged
merged 5 commits into from
Oct 17, 2019
Merged

Add subscriptions #3390

merged 5 commits into from
Oct 17, 2019

Conversation

kinow
Copy link
Member

@kinow kinow commented Sep 30, 2019

This is a small change with no associated Issue.

Requirements check-list

  • I have read CONTRIBUTING.md and added my name as a Code Contributor.
  • Contains logically grouped changes (else tidy your branch by rebase).
  • Does not contain off-topic changes (use other PRs for other changes).
  • Appropriate tests are included (unit and/or functional).
  • Appropriate change log entry included.
  • No documentation update required.

Sibling PRs:

@kinow kinow added this to the cylc-8.0a2 milestone Sep 30, 2019
@kinow kinow self-assigned this Sep 30, 2019
@kinow
Copy link
Member Author

kinow commented Sep 30, 2019

To be updated soon. At the moment I have an issue with the context info. In the Query resolvers, the context is added in the Tornado handler.

However, that handler extends a class from graphene-tornado, which takes care to populate graphql_params, context, and other variables for the queries.

While for subscriptions, we don't extend this graphene-tornado, but tornado.web.WebsocketHandler. Meaning the info.context is not being set...

Copy link
Member Author

@kinow kinow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwsutherland finished the PR for the subscription { workflows {} }. Added some comments to the code. I think our only option would be to copy everything done for the Query, but wrapping around a while True: ... asyncio.sleep ? If you agree with that, then I will start the copy-and-paste and finish updating this PR.

args['exworkflows'] = [parse_workflow_id(w_id) for w_id in
args['exids']]
resolvers = info.context.get('resolvers')
yield await resolvers.get_workflows(args)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty much the same as get_workflows - used for a Query.

With a difference that we return the coroutine here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could move the argument parsing to a common function called by both.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, but then we would also have to do that for all other resolvers?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, all if it’s worth it... but if we move argument parsing to resolvers.py it would serve the same purpose... Then the schema would be pure schema

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then the schema would be pure schema

Ah, I think I'm starting to understand it. This would simplify the schema a bit more. Would it be alright to move this to a follow-up PR?

Copy link
Member

@dwsutherland dwsutherland Oct 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it also might open other avenues, hiding the nuts and bolts 🔩 of the driving data... i.e. defining the schema in GraphQL pseudo code, like a document which is not implementation language specific (which is a thing)

(Not that we want to go that way)

args['exids']]
resolvers = info.context.get('resolvers')
yield await resolvers.get_workflows(args)
await asyncio.sleep(5.)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most examples in graphql-ws use a 1. interval. However, doing so would send data to the UI too fast I think (?). At least for now, until we have the deltas and/or update the UI to handle updates selectively (i.e. update the workflow/graph data structure on the UI only if it's different.. though it'd be a bit hard for the tree at least)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've set it to 5 seconds... which isn't really much different than the polling we have right now 😬 but one step each time 🚶‍♂️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depends on the query I suppose, and whether it's only updates feed to the resolver.. i.e.

query { workflows { id } }

would be ok.. All this will be driven by data updates in the future (which could be sub 1sec 😬 )

Copy link
Member

@dwsutherland dwsutherland Sep 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't just send updated content then, this is another reason why views just subscribing to exactly what they need might be better (so not all data is sent every push)..

description=Workflow._meta.description,
ids=List(ID, default_value=[]),
exids=List(ID, default_value=[]),
resolver=subscribe_workflows)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty much the same as the class Queries we have already for a Query. Only difference being the resolver...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplication is ok..

resolver=subscribe_workflows)


schema = Schema(query=Queries, subscription=Subscriptions, mutation=Mutations)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema untouched, with exception of a new subscription= value 👍 then the schema "understands" queries with subcription { ... }

while True:
yield await func(*args, **kwargs)
await asyncio.sleep(sleep_seconds)
return gen
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwsutherland a function that converts the existing resolver methods into async-generators, expected for the websockets to work.

description=Workflow._meta.description,
ids=List(ID, default_value=[]),
exids=List(ID, default_value=[]),
resolver=to_subscription(get_workflows))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwsutherland now I can query

subscription { workflows }

Or

subscription { tasks }

However, querying

subscription { workflows { tasks } }

Doesn't bring me anything. Any clue what I could be missing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will have a look

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, never mind. Just realized why that's happening. In the class Workflow(ObjectType), it has the Tasks and the resolver is set there too. Hmmm, tricky to get this one working for subscriptions now 😞

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think someone had the same issue with the aiohttp version:
graphql-python/graphql-ws#12 (comment)

Looks as though they modified the handler to await all async nested fields..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 gonna read it again tomorrow. That method could be a life saver. Just need to understand the logic and see if that'd work for us too. Great find @dwsutherland !

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwsutherland , quick test, and that actually worked. Will update the PR (just clean up current mess, move customizations to a separate file, etc) in a few minutes. Understood about 50% of the code, but will re-read it in the morning ☕ Thanks a ton for that. Otherwise I would still be scratching my head with no solution.

@dwsutherland
Copy link
Member

dwsutherland commented Oct 2, 2019

Works great! thanks @kinow.. Tested it also:
image
You can have multiple subscriptions via new tabs, however, I think you need to rename the root name to modify it as I guess the previous one is still running? i.e.

subscription {
  jobs2: jobs (workflows: ["*|*"], states: ["submitted", "running"]) {
    id
    state
    submitNum
    batchSysName
    batchSysJobId
  }
}

After the nested async issue is solved, we can probably try get this one in...

We can modify & organise the code where needed afterwards... This will at least allow the UI to change to the new protocol (not sure if the proxy needs modifying?)

@kinow
Copy link
Member Author

kinow commented Oct 2, 2019

Was going to add a changelog for 8.0a2, but looks like we don't have a changelog for 8.0a1, only for 8.0a0. Should we add one retroactively @cylc/core ?

@kinow kinow marked this pull request as ready for review October 3, 2019 00:17
@kinow
Copy link
Member Author

kinow commented Oct 3, 2019

Just tried writing a very simple functional test. Copied 01-workflow.t and its suite directory.

Unfortunately it doesn't look like cylc client 'graphql' .... works with subscriptions. I am getting:

$ cylc client five 'graphql' < <(echo $JSON)
[
    {
        "error": {
            "message": "Subscriptions are not allowed. You will need to either use the subscribe function or pass allow_subscriptions=True",
            "traceback": [
                "Traceback (most recent call last):\n",
                "  File \"/home/kinow/Development/python/workspace/cylc-flow/venv/lib/python3.7/site-packages/promise/promise.py\", line 85, in try_catch\n    return (handler(*args, **kwargs), None)\n",
                "  File \"/home/kinow/Development/python/workspace/cylc-flow/venv/lib/python3.7/site-packages/graphql/execution/executor.py\", line 127, in promise_executor\n    return execute_operation(exe_context, exe_context.operation, root)\n",
                "  File \"/home/kinow/Development/python/workspace/cylc-flow/venv/lib/python3.7/site-packages/graphql/execution/executor.py\", line 176, in execute_operation\n    \"Subscriptions are not allowed. \"\n",
                "Exception: Subscriptions are not allowed. You will need to either use the subscribe function or pass allow_subscriptions=True\n"
            ]
        }
    }
]

Which is a command similar to what test_header translates the functional test to. The subscription is handled in cylc-uiserver, not in cylc-flow... from what I understand, cylc-uiserver's Tornado handler will receive a message via WebSockets, receive it, and then will use Cylc Flow's schema to resolve it.

So I think we only need the unit test here for the schema.

@kinow
Copy link
Member Author

kinow commented Oct 3, 2019

Coverage report not passing the threshold, but looking at the diff doesn't seem to have much else that can be tested without actually using a websocket + the ioloop to consume the async-generator 😥

@dwsutherland
Copy link
Member

dwsutherland commented Oct 6, 2019

Was going to add a changelog for 8.0a2, but looks like we don't have a changelog for 8.0a1, only for 8.0a0. Should we add one retroactively @cylc/core ?

I think it's Ok that we don't hit the 95% mark with this one.. Because adding functioning GraphQL subscriptions at the cylc-flow/WS end is/could-be a different PR, and has to wait until #3389 is in (if it's going to use PUB/SUB).

This PR is at minimum for adding UIS subscriptions (although you could wait, and construct the handler after #3389 )...

Proper GraphQL subscriptions would be data-driven, not just a loop in the schema calling the query (IMO).

@hjoliver
Copy link
Member

hjoliver commented Oct 7, 2019

Was going to add a changelog for 8.0a2, but looks like we don't have a changelog for 8.0a1, only for 8.0a0. Should we add one retroactively @cylc/core ?

We do have a section in CHANGES.md for 8.0a1 - maybe your branch was out of date?

I've added one for 8.0a2 in #3402

@kinow
Copy link
Member Author

kinow commented Oct 7, 2019

Was going to add a changelog for 8.0a2, but looks like we don't have a changelog for 8.0a1, only for 8.0a0. Should we add one retroactively @cylc/core ?

We do have a section in CHANGES.md for 8.0a1 - maybe your branch was out of date?

I've added one for 8.0a2 in #3402

Oh, I think I simply missed that, sorry. 👏 👏 will add one for the subscriptions now. Thanks!

@kinow
Copy link
Member Author

kinow commented Oct 7, 2019

(change rebased onto master, no other code changes today 👍 )

@kinow
Copy link
Member Author

kinow commented Oct 7, 2019

<post-rebase-travis-kick />

Copy link
Member

@dwsutherland dwsutherland left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works for me! 🎉 ... We can make the push data driven in future PRs..

As long as tests pass, good to merge for me..

@kinow
Copy link
Member Author

kinow commented Oct 10, 2019

Kicked Travis (quite sure it passed before, probably because I rebased it).

Copy link
Member

@hjoliver hjoliver left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@hjoliver hjoliver merged commit 8564007 into cylc:master Oct 17, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants