-
Notifications
You must be signed in to change notification settings - Fork 1k
async datastore in modbus server #2127
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
async datastore in modbus server #2127
Conversation
46e1932 to
ecbdd74
Compare
janiversen
left a comment
There was a problem hiding this 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.
pymodbus/client/base.py
Outdated
| returns number of bytes consumed | ||
| """ | ||
| self.framer.processIncomingPacket(data, self._handle_response, slave=0) | ||
| task = asyncio.create_task( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
Also once the implementation is OK, you need to add test cases, that show the new code works. |
88ff71e to
9a193ad
Compare
| return True | ||
|
|
||
| def getValues(self, fc_as_hex, _address, _count=1): | ||
| async def async_getValues(self, fc_as_hex, address, count=1): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
janiversen
left a comment
There was a problem hiding this 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.
9a193ad to
030439b
Compare
|
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
030439b to
ca7bbeb
Compare
|
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. |
janiversen
left a comment
There was a problem hiding this 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): |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
|
your description says: 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 |
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.
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. |
That is basically correct, but execute is also used in the client code.
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.
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. |
Ok, that sounds good starting point to recheck things.
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.
I'll check if I can do something to keep async_execute and execute with minimal code duplication. |
|
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. |
|
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:
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. |
|
I'll split the things to 4 PR's and link those here. |
|
Intent of this PR is splitted to separated PR's and linked here, so closing this one as it has ran it course |
Changed datastore to be used by async-flow by modbus server, allowing datastores to be async related to getValues/setValues use.