Skip to content

Conversation

@dwsutherland
Copy link
Member

@dwsutherland dwsutherland commented Mar 10, 2025

closes #333
closes #388
closes #698

Is a sibling to:
cylc/cylc-flow#6478
cylc/cylc-ui#2091
(cylc-flow/cylc-uiserver PR needed to be merged at the same time)

Tests will fail until cylc-flow end is in.

Dependencies on:

graphene-tornado==2.6.*
graphql-ws==0.4.4

have been removed and/or rewritten as a Cylc port of active projects.

There are some short comings in graphene==3.4.3 with the use of middleware and our bespoke execution context (for null-stripping fields) in subscriptions. So I had to adopt and augment a graphql-core package:
https://github.com/graphql-python/graphql-core/blob/v3.2.6/src/graphql/execution/subscribe.py
to our own:
cylc/uiserver/graphql/subscribe.py

The HEAD of graphql-core has this remedied, but both that and the a corresponding graphene will need to be released.
Until then cylc-flow has graphene fixed at 3.4.3, as we'll need to update our code with any future release of graphene/graphql-core (hopefully dropping this workaround).

All aspects appear to be functional:

graphql-core-v3

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).
  • Applied any dependency changes to both setup.cfg (and conda-environment.yml if present).
  • Tests are included (or explain why tests are not needed).
  • Changelog entry included if this is a change that can affect users

Copy link
Member

@oliver-sanders oliver-sanders left a comment

Choose a reason for hiding this comment

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

🚀

@dwsutherland dwsutherland force-pushed the graphql-core-v3 branch 2 times, most recently from 03cd6f7 to f0daa30 Compare March 13, 2025 10:13
@oliver-sanders

This comment was marked as resolved.

MetRonnie

This comment was marked as resolved.

@MetRonnie

This comment was marked as resolved.

@dwsutherland

This comment was marked as resolved.

@dwsutherland dwsutherland mentioned this pull request Mar 24, 2025
2 tasks
@dwsutherland dwsutherland force-pushed the graphql-core-v3 branch 2 times, most recently from ac0589e to f23ce2b Compare May 13, 2025 09:02
@oliver-sanders
Copy link
Member

oliver-sanders commented Jun 19, 2025

gdammit.

Looks like this works under graphql-core v2 but not v3?!

The message sounds like it is complaining about the use of a list as a default value, however, we do the exact same thing with broadcasts just a couple of lines above?!

    broadcasts = GenericScalar(
        ids=graphene.List(
            ID,
            description=sstrip('''
                Node IDs, cycle point and/or-just family/task namespace.

                E.g: `["1234/foo", "1234/FAM", "*/FAM"]`
            '''),
            default_value=[]  # OK?
        ),
        resolver=resolve_broadcasts,
        description='Any active workflow broadcasts.'
    )
    pruned = Boolean()  # TODO: what is this? write description
    n_edge_distance = Int(
        description=sstrip('''
            The maximum graph distance (n) from an active node
            of the data-store graph window.
        '''),
    )
    log_records = graphene.List(
        LogRecord,
        description=sstrip('''
            Scheduler log messages of level WARNING and above.

            Warning: This is an incrementally updated field. Each update
            contains a list of **new** log messages, not a list of all
            messages. It is down to the client to store / preserve previous
            records.
        '''),
        default_value=[],  # Not OK?!
    )

@oliver-sanders
Copy link
Member

This seems to do the trick: cylc/cylc-flow#6803

@oliver-sanders
Copy link
Member

Integration failure fixed, poking tests...

@dwsutherland
Copy link
Member Author

dwsutherland commented Jun 23, 2025

The message sounds like it is complaining about the use of a list as a default value, however, we do the exact same thing with broadcasts just a couple of lines above?!

I guess default values on arguments are treated different to those on the fields?
Funny thing is, the error is complaining about an immutable type:

ValueError: mutable default <class 'list'> for field log_records is not allowed: use default_factory

and default_factory isn't a graphene option

../../.envs/uis-gql/lib/python3.10/site-packages/graphene/types/argument.py:112: in to_arguments
    raise ValueError(f'Unknown argument "{default_name}".')
E   ValueError: Unknown argument "default_factory".

but if you make it a immutable type:

        default_value=(),

It doesn't complain 😆 (also default_value=None works) .. Maybe a bug? idk
(I had to remove similar default_value=[] elsewhere BTW)

However, I think the field default is implicit? (if there is nothing but you ask for the field it will return [] not null)
So removing it is probably the right thing to do..

@dwsutherland
Copy link
Member Author

dwsutherland commented Jun 27, 2025

I've been trying to get the subscription test working, so far I have got to using:

@pytest.fixture
def gql_sub(jp_ws_fetch):
    """Perform a GraphQL request.
    E.G:
    conn = await gql_sub(
        *('cylc', 'subscriptions'),
        query='''
            subscription {
                workflows {
                    id
                    status
                }
            }
        ''',
    )
    while True:
        msg = await conn.read_message()
        # . . .
    """
    async def _fetch(*endpoint, query=None, headers=None):
        headers = headers or {}
        #headers = {
        #    **headers,
        #    'Content-Type': 'application/json'
        #}
        return await jp_ws_fetch(
            *endpoint,
            headers=headers,
            params={'query': query},
            subprotocols=['graphql-ws'],
        )
    return _fetch
async def test_subscription(gql_sub, dummy_workflow):
    """Test sending the most basic GraphQL subscription."""
    # configure two dummy workflows so we have something to look at
    await dummy_workflow('foo')
    await dummy_workflow('bar')
    # perform the request
    conn = await gql_sub(
        *('cylc', 'subscriptions'),
        query='''
            subscription {
                workflows {
                    id
                    status
                }
            }
        ''',
    )
    print(conn)
    while True:
        msg = await conn.read_message()
        print(msg)
        break
    assert False

However, I must be doing something wrong.. as it hangs on the conn = await gql_sub ... Ref to:
https://www.tornadoweb.org/en/stable/_modules/tornado/websocket.html#websocket_connect
and
https://github.com/jupyter-server/pytest-jupyter/blob/main/pytest_jupyter/jupyter_server.py

maybe I have to do something like:

with await gql_sub(...) as conn:
    ...

Will have another crack at it next week...

@oliver-sanders
Copy link
Member

oliver-sanders commented Jun 27, 2025

If this is a new test, maybe remove it from this PR and prepare a new PR to add the tests?

Comment on lines 335 to 337
if op_id is None or connection_context.has_operation(op_id):
message = self.build_message(op_id, op_type, payload)
return await connection_context.send(message)
Copy link
Member

@MetRonnie MetRonnie Jun 27, 2025

Choose a reason for hiding this comment

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

Not a new problem in this PR, but I have figured out why we don't get any visibility of subscription errors anywhere. It's because this function doesn't do anything if the operation ID is not registered in the connection context, which it isn't for a graphql subscription error.

But why doesn't it do anything? As far as I can tell, neither self.build_message nor connection_context.send make use of the iterator registered with the operation ID, so why not just do?

Suggested change
if op_id is None or connection_context.has_operation(op_id):
message = self.build_message(op_id, op_type, payload)
return await connection_context.send(message)
message = self.build_message(op_id, op_type, payload)
return await connection_context.send(message)

To test out what I'm talking about, try running the GUI with a typo in the Workflows.vue subscription, or by doing a GraphiQL subscription like

subscription App {
  deltas {
    id
    borked
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, yes, I get your meaning.. I think this may be inherited code, otherwise I would have thought op_id would need to be associated with a subscription channel.
image

@dwsutherland
Copy link
Member Author

dwsutherland commented Jun 30, 2025

If this is a new test, maybe remove it from this PR and prepare a new PR to add the tests?

Yes, will do.

@dwsutherland
Copy link
Member Author

dwsutherland commented Jun 30, 2025

Added a back compat patch for:
#698

image

(justification: #698 (comment))

Copy link
Member

@MetRonnie MetRonnie left a comment

Choose a reason for hiding this comment

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

🎉

@MetRonnie MetRonnie requested a review from oliver-sanders July 1, 2025 11:23
@oliver-sanders oliver-sanders merged commit 589339f into cylc:master Jul 7, 2025
14 of 16 checks passed
@oliver-sanders
Copy link
Member

🎉

@MetRonnie MetRonnie mentioned this pull request Jul 7, 2025
6 tasks
@hjoliver
Copy link
Member

hjoliver commented Jul 8, 2025

🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Object of type RequestError is not JSON serializable Python 3.10 support dependency sustainability

5 participants