Hi Steven,
When returning a python object from an RtdPublisher without a converter, the automatic cache key generation ends up using a shared cache for all publishers publishing that object type. This results in values from different topics appearing in the wrong cells.
Details
We call FromPyObj() in publish which eventually calls ReturnToCache()
|
xlValue = converter |
|
? (*converter)(ptr) |
|
: FromPyObj()(ptr); |
Since the caching isn't being called from a cell, xlfCaller fails to return an address in CallerInfo() which causes us to fall back to "Unknown" in writeAddressImpl. The value is then cached and the cache key is broadcast.
|
CallerInfo::CallerInfo() |
|
{ |
|
callExcelRaw(xlfCaller, &_address); |
|
callExcelRaw(xlSheetNm, &_sheetName, |
|
_address.isType(ExcelType::RangeRef) ? &_address : &theA1Ref); |
|
} |
|
default: // Other callers |
|
constexpr wchar_t nonWorksheetCaller[] = L"Unknown"; |
When rtdpeek or rtdsubscribe get a value like 心[nknownExample,C, they will look to the cache and retrieve a value from the cache and then replace it back into their respective cellcache but with many publishers, it's likely the value was intended for another topic.
Repro
The following formulas can be copied into and new workbook with the second row dragged down a handful of times to increase the load.
blank|=ROW(A1)&";"&COLUMN(A1) |=RtdExample(B1)|=xloAttr(C1,"value")|=COUNTA(UNIQUE(D:D))|=COUNTA(UNIQUE(B:B))
blank|=ROW(A2)&";"&COLUMN(A2) |=RtdExample(B2)|=xloAttr(C2,"value")|
import asyncio
from dataclasses import dataclass
import time
import xloil as xlo
@dataclass
class Example:
value: str
if "_rtdServer" not in globals():
# don't recreate on reload!
_rtdServer = xlo.RtdServer()
class RtdPublisherExample(xlo.RtdPublisher):
"""lifted from https://xloil.readthedocs.io/en/stable/xlOil_Python/ExampleRTD.html"""
def __init__(self, input_string:str):
super().__init__() # You *must* call this explicitly or the python binding library will crash
self._python_object = Example(input_string)
self._topic = input_string
self._task = None
def connect(self, num_subscribers):
if self.done():
async def run():
try:
while True:
_rtdServer.publish(self.topic(),Example(self._topic + " " + str(time.time())))
await asyncio.sleep(0.01)
# await asyncio.sleep(1.1)
except Exception as e:
_rtdServer.publish(self._url, e)
self._task = xlo.get_event_loop().create_task(run())
def disconnect(self, num_subscribers):
if num_subscribers == 0:
self.stop()
return True # This publisher is no longer required: schedule it for destruction
def stop(self):
pass
def done(self):
return self._task is None or self._task.done()
def topic(self):
return self._topic
@xlo.func(local=False)
def RtdExample(input_string):
if _rtdServer.peek(input_string) is None:
publisher = RtdPublisherExample(input_string)
_rtdServer.start(publisher)
return _rtdServer.subscribe(input_string)
Next Steps
I took a stab at addressing this by explicitly providing the topic as a cache key which resolved the collisions between topics but didn't fully resolve the issues.
PyRtd.cpp
FromPyObj < detail::ReturnToSpecifiedCache, true > (detail::ReturnToSpecifiedCache{topic})(ptr);
BasicTypes.h ( I also added a simple pyCacheAddByKey )
/// <summary>
/// Used with FromPyObj to return unknown objects as a cache ref using a specific key rather than relying on CallerInfo
/// </summary>
struct ReturnToSpecifiedCache
{
const wchar_t* key;
// Constructor requires key to be provided and Delete default constructor to prevent uninitialized usage
explicit ReturnToSpecifiedCache(const wchar_t* k) : key(k) {}
ReturnToSpecifiedCache() = delete;
template <class TAlloc>
auto operator()(PyObject* obj, const TAlloc& stringAllocator)
{
return ExcelObj(BasicPString<wchar_t, TAlloc>(pyCacheAddByKey(PyBorrow(obj),key).asStringView(), stringAllocator));
}
};
However I see an issue with this when the excel calc frequency is less than the RTD publisher frequency. When multiple values are cached to the same cellcache we increment the cache counter.
RTD peek will fetch the cache key (eg 心[topicABC,C ) on line 328 but not the cached value. If the calc cycle has ticked forward and the cache was just cleared then there may not be enough values when we use the cache key to retrieve the python object.
|
py::object peek(const wchar_t* topic, IPyFromExcel* converter = nullptr) |
|
{ |
|
shared_ptr<const ExcelObj> value; |
|
{ |
|
py::gil_scoped_release releaseGil; |
|
value = impl().peek(topic); |
|
} |
|
if (!value) |
|
return py::none(); |
|
return PySteal<>(converter |
|
? (*converter)(*value) |
|
: PyFromAny()(*value)); |
|
} |
I think we can address this by not allowing multiple separate auto cached values for RTDPublishers and to limit the cache size to 1. iterables of cacheables would need be cached directly rather than be split out.
I recall previously you mentioned you had thoughts of a grander overhaul of caching. I'd be very interested to hear your thoughts on how you'd like to approach this. In the mean time I'll attempt getting a POC PR assembled.
As always hope all is well and appreciate you sharing this library!
Hi Steven,
When returning a python object from an RtdPublisher without a converter, the automatic cache key generation ends up using a shared cache for all publishers publishing that object type. This results in values from different topics appearing in the wrong cells.
Details
We call
FromPyObj()inpublishwhich eventually callsReturnToCache()xloil/libs/xlOil_Python/PyRtd.cpp
Lines 301 to 303 in 6d4ba1b
Since the caching isn't being called from a cell,
xlfCallerfails to return an address inCallerInfo()which causes us to fall back to"Unknown"inwriteAddressImpl. The value is then cached and the cache key is broadcast.xloil/src/xlOil-XLL/Caller.cpp
Lines 396 to 401 in 6d4ba1b
xloil/src/xlOil-XLL/Caller.cpp
Lines 380 to 381 in 6d4ba1b
When rtdpeek or rtdsubscribe get a value like
心[nknownExample,C, they will look to the cache and retrieve a value from the cache and then replace it back into their respective cellcache but with many publishers, it's likely the value was intended for another topic.Repro
The following formulas can be copied into and new workbook with the second row dragged down a handful of times to increase the load.
Next Steps
I took a stab at addressing this by explicitly providing the topic as a cache key which resolved the collisions between topics but didn't fully resolve the issues.
PyRtd.cpp
FromPyObj < detail::ReturnToSpecifiedCache, true > (detail::ReturnToSpecifiedCache{topic})(ptr);BasicTypes.h ( I also added a simple
pyCacheAddByKey)However I see an issue with this when the excel calc frequency is less than the RTD publisher frequency. When multiple values are cached to the same cellcache we increment the cache counter.
RTD peek will fetch the cache key (eg
心[topicABC,C) on line 328 but not the cached value. If the calc cycle has ticked forward and the cache was just cleared then there may not be enough values when we use the cache key to retrieve the python object.xloil/libs/xlOil_Python/PyRtd.cpp
Lines 323 to 335 in 6d4ba1b
I think we can address this by not allowing multiple separate auto cached values for RTDPublishers and to limit the cache size to 1. iterables of cacheables would need be cached directly rather than be split out.
I recall previously you mentioned you had thoughts of a grander overhaul of caching. I'd be very interested to hear your thoughts on how you'd like to approach this. In the mean time I'll attempt getting a POC PR assembled.
As always hope all is well and appreciate you sharing this library!