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 signal connection cache #368

Merged
merged 15 commits into from
Jun 26, 2024
Merged

Conversation

stan-dot
Copy link
Contributor

@stan-dot stan-dot commented Jun 5, 2024

No description provided.

@stan-dot stan-dot self-assigned this Jun 5, 2024
@stan-dot stan-dot linked an issue Jun 5, 2024 that may be closed by this pull request
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Show resolved Hide resolved
@stan-dot stan-dot changed the title initial draft for signal connection cache Add signal connection cache Jun 5, 2024
@stan-dot
Copy link
Contributor Author

stan-dot commented Jun 5, 2024

re: testing the signal. how should I instantiate the object? with epics_signal_rw or the constructor?

@coretl

@stan-dot
Copy link
Contributor Author

stan-dot commented Jun 6, 2024

Copy link
Collaborator

@coretl coretl left a comment

Choose a reason for hiding this comment

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

You can also remove the parameterization from

@pytest.mark.parametrize("connect_mock_mode", [True, False])
async def test_mock_signal_backend(connect_mock_mode):
mock_signal = SignalRW(MockSignalBackend(datatype=str))
# If mock is false it will be handled like a normal signal, otherwise it will
# initalize a new backend from the one in the line above
await mock_signal.connect(mock=connect_mock_mode)
assert isinstance(mock_signal._backend, MockSignalBackend)
assert await mock_signal._backend.get_value() == ""
await mock_signal._backend.put("test")
assert await mock_signal._backend.get_value() == "test"
assert mock_signal._backend.put_mock.call_args_list == [
call("test", wait=True, timeout=None),
]
and set mock=True always to make that test work

src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
tests/core/test_signal.py Outdated Show resolved Hide resolved
@stan-dot stan-dot requested a review from coretl June 7, 2024 08:50
@stan-dot
Copy link
Contributor Author

the error string does not appear to be present anywhere but in the test. not sure where is it coming from .
image

@stan-dot
Copy link
Contributor Author

stan-dot commented Jun 11, 2024

async def test_signal_can_be_given_backend_on_connect():
    sim_signal = SignalR()
    backend = MockSignalBackend(int)
    assert sim_signal._backend is None
    await sim_signal.connect(mock=False, backend=backend)
    assert await sim_signal.get_value() == 0

what is the goal of this test? right now it clashes with the caching implementation. we can either stop supporting this use case and drop the test (easy) or somehow change it OR the implementation of the feature (hard)

that test was added 6 months ago by Rose

@coretl
Copy link
Collaborator

coretl commented Jun 11, 2024

that test was added 6 months ago by Rose

Looks like it was added quite recently to support supplying backend on connect:
image

We have to keep the test.

I suggest we always force a reconnect if backend is supplied, as this will only be used if the Signal is part of a Device, in which case the caching will happen there.

@evalott100 do you agree?

@stan-dot
Copy link
Contributor Author

stan-dot commented Jun 11, 2024

support supplying backend on connect:

I think I am missing some context. what does this do, when it is used and why do we support it?

@coretl
Copy link
Collaborator

coretl commented Jun 11, 2024

support supplying backend on connect:

I think I am missing some context. what does this do, when it is used and why do we support it?

This is used when we know that a Signal will exist, like in

class SeqBlock(Device):
table: SignalRW[SeqTable]
active: SignalRW[bool]
repeats: SignalRW[int]
prescale: SignalRW[float]
prescale_units: SignalRW[TimeUnits]
enable: SignalRW[str]
but we don't know what PV it will connect to until connect(). What we will do is create the given SignalRW with backend=None, then on Device.connect() we actually create the backend and pass it to Signal.connect().

@stan-dot
Copy link
Contributor Author

huh, I'd never have guessed that

@stan-dot
Copy link
Contributor Author

should we have a flag for a case like that? it looks quite unusual. I'm thinking a flag next to 'force_reconnect' and 'mock'. then we can do logic flow around that.

@coretl
Copy link
Collaborator

coretl commented Jun 12, 2024

should we have a flag for a case like that? it looks quite unusual. I'm thinking a flag next to 'force_reconnect' and 'mock'. then we can do logic flow around that.

The flag is a supplied backend. If backend is not None we should force a reconnect, otherwise do the existing behaviour

@stan-dot
Copy link
Contributor Author

stan-dot commented Jun 12, 2024

this is the error I get, but it looks like expected:

File "/workspaces/ophyd-async/tests/core/test_signal.py", line 80, in test_signal_connect_fails_if_different_backend_but_same_by_value
    assert str(exc.value) == "Backend at connection different from initialised one."
AssertionError: assert 'Cannot make a MockSignalBackend for a MockSignalBackends' == 'Backend at connection different from initialised one.'
  
  - Backend at connection different from initialised one.
  + Cannot make a MockSignalBackend for a MockSignalBackends

@stan-dot
Copy link
Contributor Author

image

these two are too similar, I'd delete the second one or change the expected message. because the value is different.

@stan-dot
Copy link
Contributor Author

image

@stan-dot stan-dot requested a review from coretl June 13, 2024 08:53
@evalott100
Copy link
Contributor

what is the goal of this test? right now it clashes with the caching implementation. we can either stop supporting this use case and drop the test (easy) or somehow change it OR the implementation of the feature (hard)

It tests that you can choose to pass the signal backend in at runtime - why would this be hard to support with your changes?

@evalott100
Copy link
Contributor

these two are too similar, I'd delete the second one or change the expected message. because the value is different.

I don't mind putting them in the same test but it's still nicer to check that equality by value and by ref

@stan-dot
Copy link
Contributor Author

@evalott100 now the latest version has both tests, just the expected string is different for the second test

@evalott100
Copy link
Contributor

@evalott100 now the latest version has both tests, just the expected string is different for the second test

That makes sense, you got rid of the validation on the backends in the new connect logic

@stan-dot
Copy link
Contributor Author

ok, trying to make this right now

@stan-dot stan-dot requested a review from evalott100 June 20, 2024 13:57
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
tests/core/test_signal.py Outdated Show resolved Hide resolved
tests/core/test_signal.py Outdated Show resolved Hide resolved
tests/core/test_signal.py Outdated Show resolved Hide resolved
Comment on lines 149 to 159
await mock_signal_rw.connect(mock=False)
RE(ensure_connected(mock_signal_rw, mock=True))
assert (
mock_signal_rw._connect_task
and mock_signal_rw._connect_task.done()
and not mock_signal_rw._connect_task.exception()
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
await mock_signal_rw.connect(mock=False)
RE(ensure_connected(mock_signal_rw, mock=True))
assert (
mock_signal_rw._connect_task
and mock_signal_rw._connect_task.done()
and not mock_signal_rw._connect_task.exception()
)
await signal_rw.connect()
RE(ensure_connected(signal_rw))
assert (
mock_signal_rw._connect_task
and mock_signal_rw._connect_task.done()
and not mock_signal_rw._connect_task.exception()
)

This test would have misleadingly failed I assume... The second connect wouldn't have succeeded since mock changed value, however it wouldn't have updated the connect_task so the assert here would have shown the first connect task was successful and passed.

I'm assuming you're choosing a soft signal over a mock signal because you want to mimick the performance of real signals within the connect? I'd like to see a version of this test where you use a real pva signal, passing mock=True on both connects.

Edit

Because of the unset _previous_connect_was_mock we don't get failure from changing mocks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm assuming you're choosing a soft signal over a mock signal because you want to mimick the performance of real signals within the connect? I'd like to see a version of this test where you use a real pva signal, passing mock=True on both connects.

so mock=True creates a 'real pva signal'? I lost the plot

src/ophyd_async/core/signal.py Show resolved Hide resolved
tests/core/test_signal.py Outdated Show resolved Hide resolved
tests/core/test_signal.py Outdated Show resolved Hide resolved
@stan-dot stan-dot requested a review from evalott100 June 24, 2024 13:36
Copy link
Contributor

@evalott100 evalott100 left a comment

Choose a reason for hiding this comment

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

I made a commit causing Device to fail on connect if mock changes value. Realised it was probably nicer to fail outright for Devices too.

A couple more changes left.

src/ophyd_async/core/signal.py Outdated Show resolved Hide resolved
Comment on lines 150 to 174
async def test_signal_lazily_connects(RE):
mock_signal_rw = soft_signal_rw(int, 0, name="mock_signal")
await mock_signal_rw.connect(mock=False)
RE(ensure_connected(mock_signal_rw, mock=True))
assert (
mock_signal_rw._connect_task
and mock_signal_rw._connect_task.done()
and not mock_signal_rw._connect_task.exception()
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Please respond to this

https://github.com/bluesky/ophyd-async/pull/368/files#r1650538856

This test would have misleadingly failed I assume... The second connect wouldn't have succeeded since mock changed value, however it wouldn't have updated the connect_task so
the assert here would have shown the first connect task was successful and passed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that test is the only one that uses ensure_connected, but seems to pass even if I change is as much as here:
image

Copy link
Contributor

@evalott100 evalott100 Jun 25, 2024

Choose a reason for hiding this comment

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

#368 (comment)

Same as above comment. This will always work because it's a soft_signal - but we want it to fail on the first connect.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, I see now - so here we test a scenario where the device rejects at first but later accepts.

I think the best way to do this is to have a mock device that has its own connect with this 'fail on first connect' functionality, to mimic a real life device that misbehaves when we try to connect to it. So a similar class to this one:

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Do it by swapping out a MockSignalBackend's connect. That way you'll still be using the regular connect method/functionality of Device and Signal.

tests/core/test_signal.py Outdated Show resolved Hide resolved
Copy link
Contributor

@evalott100 evalott100 left a comment

Choose a reason for hiding this comment

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

Nice job! I don't know if we want to wait for @coretl to give this a once over before merging, but I'm happy with it.

@stan-dot stan-dot force-pushed the 313-add-connect-caching-to-signal branch from cd9b7bc to 264f16d Compare June 26, 2024 14:46
Copy link
Collaborator

@coretl coretl left a comment

Choose a reason for hiding this comment

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

One minor comment, otherwise good to go

src/ophyd_async/core/signal.py Show resolved Hide resolved
@stan-dot stan-dot requested a review from coretl June 26, 2024 15:36
@stan-dot stan-dot merged commit f8009fd into main Jun 26, 2024
18 checks passed
@stan-dot stan-dot deleted the 313-add-connect-caching-to-signal branch June 26, 2024 15:52
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.

Add connect caching to Signal
3 participants