Skip to content

graphql-core v3 upgrade - #672

Merged
oliver-sanders merged 14 commits into
cylc:masterfrom
dwsutherland:graphql-core-v3
Jul 7, 2025
Merged

graphql-core v3 upgrade#672
oliver-sanders merged 14 commits into
cylc:masterfrom
dwsutherland:graphql-core-v3

Conversation

@dwsutherland

@dwsutherland dwsutherland commented Mar 10, 2025

Copy link
Copy Markdown
Member

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

Comment thread cylc/uiserver/graphql/render_graphiql.py Outdated

@oliver-sanders oliver-sanders left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🚀

Comment thread cylc/uiserver/app.py Outdated
Comment thread cylc/uiserver/graphql/render_graphiql.py Outdated
Comment thread cylc/uiserver/graphql/render_graphiql.py Outdated
Comment thread cylc/uiserver/graphql/subscribe.py Outdated
Comment thread cylc/uiserver/graphql/subscribe.py Outdated
Comment thread cylc/uiserver/graphql/tornado.py
Comment thread cylc/uiserver/graphql/tornado_ws.py Outdated
Comment thread cylc/uiserver/graphql/tornado_ws.py
Comment thread cylc/uiserver/handlers.py Outdated
Comment thread cylc/uiserver/graphql/__init__.py
@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.

Comment thread cylc/uiserver/graphql/tornado_ws.py Outdated
@dwsutherland

This comment was marked as resolved.

@dwsutherland dwsutherland mentioned this pull request Mar 24, 2025
2 tasks
Comment thread cylc/uiserver/graphql/tornado.py
Comment thread cylc/uiserver/graphql/tornado_ws.py Outdated
Comment thread cylc/uiserver/schema.py Outdated
@dwsutherland
dwsutherland force-pushed the graphql-core-v3 branch 2 times, most recently from ac0589e to f23ce2b Compare May 13, 2025 09:02
Comment thread cylc/uiserver/graphql/tornado.py
@oliver-sanders

oliver-sanders commented Jun 19, 2025

Copy link
Copy Markdown
Member

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
Copy Markdown
Member

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

@oliver-sanders

Copy link
Copy Markdown
Member

Integration failure fixed, poking tests...

@dwsutherland

dwsutherland commented Jun 23, 2025

Copy link
Copy Markdown
Member Author

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..

Comment thread cylc/uiserver/tests/test_schema.py Outdated
@dwsutherland

dwsutherland commented Jun 27, 2025

Copy link
Copy Markdown
Member Author

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

oliver-sanders commented Jun 27, 2025

Copy link
Copy Markdown
Member

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

Comment thread cylc/uiserver/graphql/tornado_ws.py Outdated
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)

@MetRonnie MetRonnie Jun 27, 2025

Copy link
Copy Markdown
Member

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
Copy Markdown
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 and others added 2 commits June 30, 2025 21:29
Co-authored-by: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com>
@dwsutherland

dwsutherland commented Jun 30, 2025

Copy link
Copy Markdown
Member Author

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

dwsutherland commented Jun 30, 2025

Copy link
Copy Markdown
Member Author

Added a back compat patch for:
#698

image

(justification: #698 (comment))

Comment thread cylc/uiserver/graphql/tornado.py

@MetRonnie MetRonnie left a comment

Copy link
Copy Markdown
Member

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
@oliver-sanders

Copy link
Copy Markdown
Member

🎉

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

hjoliver commented Jul 8, 2025

Copy link
Copy Markdown
Member

🚀

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