Skip to content

Conversation

@ilkka-ollakka
Copy link
Contributor

@ilkka-ollakka ilkka-ollakka commented Mar 28, 2024

Changed datastore to be used by async-flow by modbus server, allowing datastores to be async related to getValues/setValues use.

@ilkka-ollakka ilkka-ollakka force-pushed the feat/async_pymodbus branch 3 times, most recently from 46e1932 to ecbdd74 Compare March 28, 2024 12:39
Copy link
Collaborator

@janiversen janiversen left a comment

Choose a reason for hiding this comment

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

The idea of making datastore async is something on our wish list, but I see multiple problems with your PR:

  • Client code is changed (datastore is solely for server)
  • Async are added in several places, breaking the sync client
  • There are far too many files changed for a simple datastore change.

I think a much simpler approach would be to add 2 new function in datastore:

  • async_getValues() and async_setValues
    That way most of your changes outside datastore is not needed.
    The 2 new functions can simply call the old getValues() / setValues()

Apart from that the CI is pretty red, something that needs to be solved.

returns number of bytes consumed
"""
self.framer.processIncomingPacket(data, self._handle_response, slave=0)
task = asyncio.create_task(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is client code and have nothing to do with the datastore which is purely server.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ahh I see that CI is now Green, one important step done.

You still need to limit your changes, especially so that NO client code is included.

But let me repeat, the idea of the PR is good and very welcome.

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 think I checked that sync client still works as expected, but I agree that change is quite wide.

I'll take another round to check more limited changes and try to reduce touching the client-used code changes.

Thanks for the quick comments.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am pretty sure, that adding 2 new async methods(), will meet your requirements, and limit all changes to the datastore directory + test.

Copy link
Collaborator

Choose a reason for hiding this comment

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

request me as reviewer when you are ready.

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 think the new round is closer to your proposal on approach and now it should be contained only on serverside and datastore.

@janiversen
Copy link
Collaborator

Also once the implementation is OK, you need to add test cases, that show the new code works.

@ilkka-ollakka ilkka-ollakka force-pushed the feat/async_pymodbus branch 2 times, most recently from 88ff71e to 9a193ad Compare March 30, 2024 12:07
return True

def getValues(self, fc_as_hex, _address, _count=1):
async def async_getValues(self, fc_as_hex, address, count=1):
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 left these changes as separate commit, incase there is some desire to keep this still intact.

Copy link
Collaborator

Choose a reason for hiding this comment

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

it is the other way around, we do not want the cascade changes your original PR contained and still contains.

Having the async_getValues, should solve your problem, so please revert all the cascaded changes in the system (your PR still touches 31 files).

Copy link
Collaborator

@janiversen janiversen left a comment

Choose a reason for hiding this comment

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

review note:

Please remove all the cascaded changes, they are not needed, if you do not change the original methods.

@ilkka-ollakka
Copy link
Contributor Author

I reduced the execute change to be only related the ones that call get/setvalues and server to check if it needs to await the request.

default port 8080 can be in use when running test in developer maching, so better to use port that is available
by default those just call self.set/getValues but can be used to implement more async datastore
update tests and callback code in server-code and messages
messages and serverside defaults to async-calls now and async default is to call the sync version of itself, allowing to build async datastorage functionality
@janiversen
Copy link
Collaborator

You are still touching 19 files, far too many !

You did not do, what I asked you to do, to make 2 new methods, you renamed the old ones and that causes the cascade effect.

The server code is not ready for an async datastore, until we have removed the sync parts around it. There are a lot more problems than just making the methods async.

However having async methods allowing the datastore to be accessed outside is not a problem.

Copy link
Collaborator

@janiversen janiversen left a comment

Choose a reason for hiding this comment

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

Seems you are trying to make unrelated changed in the same PR, and it seems you have not thought about how the sync client works.

ReadBitsRequestBase.__init__(self, address, count, slave, **kwargs)

def execute(self, context):
async def execute(self, context):
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is also called from the SYNC client.

if not context.validate(self.function_code, self.address, self.count):
return self.doException(merror.IllegalAddress)
values = context.getValues(self.function_code, self.address, self.count)
values = await context.async_getValues(
Copy link
Collaborator

Choose a reason for hiding this comment

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

This too is not exclusive for the server.

:param count: The number of values to retrieve
:returns: The requested values from a:a+c
"""
del fc_as_hex, address, count
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why delete the variables, that does not make a lot of sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

they are not used, so makes it more clear in that point. Mypy and ruff etc easily complain on those. But I agree it is more of taste and not really critical to be in my point of view.

Copy link
Collaborator

Choose a reason for hiding this comment

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

use "_" that is the standard way to tell a variable is unused.

It might not be critical from your POW, but from me as maintainer it is negative.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sure, I can change it to be with "_".

:param address: The starting address
:param values: The new values to be set
"""
del fc_as_hex, address, values
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here.

kwargs = {}
if self.slave:
kwargs["slave"] = self.slave
self.result = await getattr(self._client, func_fc)(address, count, **kwargs)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This looks wrong to me, the code is designed so we do not need to call getattr, which is very expensive function.

"i16": lambda a, v: self._client.write_registers(
a, v, **kwargs
),
"d5": "write_coil",
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is not the same...but might be the reason I saw a getattr earlier.

We do not want that, apart from that what does it have to do with getValues being async ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is mainly due lamdas and await don't work together. It can be redone without getattr use most likely easily.

@janiversen
Copy link
Collaborator

your description says:
"Changes codepaths to be async to allow datastore getValues/setValues to be async methods.!"

My suggestion have constantly been, add 2 new methods instead, that will allow you to have async access from outside the server.

You do not write it, but if your purpose is to write a new datastore, you are in for a far bigger job, because the server shares mixin, execute, transaction and framer code with the client both sync and async. So in order to make the datastore async internally, you need to change the server to not share code with any sync part (which includes a couple of examples). This is something that should NOT be done in a single PR, but probably at least 10 PRs

@ilkka-ollakka
Copy link
Contributor Author

your description says: "Changes codepaths to be async to allow datastore getValues/setValues to be async methods.!"

That was my original intent, to get async datastore to server. Not really to have async access from outside to datastore.

Sorry for causing frustration with my miscommunication on original description and intent related to this PR. I understand that it must have been frustrating.

You do not write it, but if your purpose is to write a new datastore, you are in for a far bigger job, because the server shares mixin, execute, transaction and framer code with the client both sync and async.
So in order to make the datastore async internally, you need to change the server to not share code with any sync part (which includes a couple of examples). This is something that should NOT be done in a single PR, but probably at least 10 PRs

From those I think only execute is actually one that blocks calling datastore as async way from server?

To be more splitted, would it be suitable approach for example to change ServerDecoder so that it returns AsyncReadCoilsRequest instead of ReadCoilsRequest etc, and those Async ones would be inherited from the original but have async execute? This way sync flow would be not touched, but it allows to take step closer to async datastore in server and work on that side without needing to touch sync execute-path?

I understand that I might not be enough, so more of mapping on things that are lacking and increasing my own understanding of the scope.

@ilkka-ollakka ilkka-ollakka changed the title async datastore/context async datastore in modbus server Mar 31, 2024
@janiversen
Copy link
Collaborator

From those I think only execute is actually one that blocks calling datastore as async way from server?

That is basically correct, but execute is also used in the client code.

To be more splitted, would it be suitable approach for example to change ServerDecoder so that it returns AsyncReadCoilsRequest instead of ReadCoilsRequest etc, and those Async ones would be inherited from the original but have async execute? This way sync flow would be not touched, but it allows to take step closer to async datastore in server and work on that side without needing to touch sync execute-path?

No we do not want to double that amount of Request/Response classes. It is clear that execute should be async when used in asyncio, so maybe a async_execute in a message base could do the trix.....this is just a thought nothing more.

I understand that I might not be enough, so more of mapping on things that are lacking and increasing my own understanding of the scope.

The easiest path, is to look at asyncio.py, because that is the whole server. You basically need to "pursuade" that file to call async_getValues instead of getValues. That is the best way (if possible), second best is to have an async_execute in 1 message base class.

But please no duplication of code and no cascading changes....allowing that is calling for trouble in future maintenance.

I think it is doable, I looked at it in depth about a year ago, when we wanted to make a better forwarder, but got stopped by more urgent matters.

@ilkka-ollakka
Copy link
Contributor Author

No we do not want to double that amount of Request/Response classes. It is clear that execute should be async when used in asyncio, so maybe a async_execute in a message base could do the trix.....this is just a thought nothing more.

Ok, that sounds good starting point to recheck things.

The easiest path, is to look at asyncio.py, because that is the whole server. You basically need to "pursuade" that file to call async_getValues instead of getValues. That is the best way (if possible), second best is to have an async_execute in 1 message base class.

Currently it does call execute and doesn't know anything about getValues/setValues, but it could be easily changed to call that async_execute. But that would cause change in some Request-classes where there is calls to getValues/setValues.

But please no duplication of code and no cascading changes....allowing that is calling for trouble in future maintenance.

I'll check if I can do something to keep async_execute and execute with minimal code duplication.

@janiversen
Copy link
Collaborator

You might need to look from where the execute is called, if it turns out to be only asyncio.py then things are simpler, but I suspect at least some axamples uses the execute.

@janiversen
Copy link
Collaborator

A short look at the code, seems to suggest that the execute in the message classes is only used in asyncio.py (it is really a wrong name if that is the case).

If this is the case, the better approach would be to have:

  • first PR, that changes the execute in asyncio to be async (hopefully without any side effects)
  • second PR, that changes the execute in all message classes to be async (This is a massive change, so it should ONLY contain edits, no functional changes)
  • third PR, that changes the datastore.

If you do it all in one PR, it is practically impossible to review, and if/when problems occur down the line, it is very difficult to pin point the cause.

@ilkka-ollakka
Copy link
Contributor Author

I'll split the things to 4 PR's and link those here.

@ilkka-ollakka
Copy link
Contributor Author

Intent of this PR is splitted to separated PR's and linked here, so closing this one as it has ran it course

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.

2 participants