# SafelyYouCodingChallenge - Michael DiLeo I did this challenge using my primary lanuage of C# to spike the implementation and then migrated to Go with AI, but asked the AI to guide me in better understanding the differences between the c# and Go code. For the C# code, I used some AI, which I will outline below, though it was much more limited. The approach that I took after scaffolding the project and creating DTO's from the openapi.json was thinking how to structure the domain. I created strong value types for things like `DeviceID`, which in C# are free for single values. I started with the device heartbeats and I initially took an approach that I removed, which was to use a `PriorityQueue` to track the history. PriorityQueue is a fairly high performance collection that can dequeue based on a priority, which I set to the inverse of the timestamp, so later entries came first. However, I did this thinking of a "wrench" situation and "what if the timestamps come out of order?" Eventually I removed that altogether and replaced it with dropping something that's earlier than the last known timestamp. I also added locking on mutating the inner collections, where a `ConcurrentDictionary` handled synchronization on creating a device and the `DeviceHeartBeatHistory` locks on mutation. I chose this path because the challenge said to keep things simple and not have a database. So, everything here is in-memory. Initially with the heartbeat history, I was tracking timestamp ranges and if a new timestamp came within the 1 minute + {some jitter offset}, it would get merged as a block of "start to end uptimes" and then an interruption would a new entry with a new start time. That is now removed, so the current code doesn't track things like if a heart beat comes too early, ie 30 seconds. The current code strongly assumes that all devices will report at 1 minute, so if a device restarts and sends a healthcheck at less than a minute, it will throw off the totals. Where I used AI was to help fix an expression compiler error in the PriorityQueue implementation, but when I decided on the new approach, I manually added the new fields and then asked the AI to migrate the logic. One other thing that I did was that I tracked timestamps using `BigInteger` instead of `long`, since Go uses nanoseconds and I wasn't sure if there'd be any gotchas. This was likely not necessary and `BigInteger` is notably slower than using `long`. But this is a habit from doing coding challenges where you just assume they're going to give you an arbitrarily large number somewhere. One other bit of AI usage was that I asked it for the format string to mimic the Go timestamp, so it quickly re-implemented the Go formatting in a long function. I thought this was hilarious and kept it. The dotnet version has more logging than the go version because it took me a while to find out that the simulator was using `/api/v1` as the route. ### Go version differences One difference in the Go version is that the `Store` shared a mutex between the heartbeats and upload histories, whereas the dotnet version uses ConcurrentDictionary on the outer and locking on the inner so there's a touch more separation there, but the mutex is still only at the top layer and child operations have their own mutex. It could be split out between the two, but for this case I think it's fine. I chose to keep the strong `DeviceId` in Go, even though it may be more idiomatic to keep it as a string, but I chose to lean more toward correctness, as a strong type allows better formatting, validation, etc. For this project, is it necessary? Not really, but it does make things a bit clearer, I think. ### Things to consider **Security** - never heard of it! Nothing is authenticated, but I'd expect a device to have a signature that is set up before hand. **How long did I spend** - several hours. Some of that was scaffolding and getting things set up. I wound up using AI to generate the DTO's because the dotnet tools wanted to generate too much, so I got tired of researching. After that was debugging and setting up the docker containers. The Go migration was pretty quick by comparison, and then lastly is writing the readme. **Other Metrics** - right now any metric expansion requires creating custom types and their own aggregations. It certainly isn't Grafana. If you're ok with the bill, exposing these items as metrics endpoints would be far easier to expand, but the custom implementations do allow for far lower data usage **Storage** - doesn't exist here, so no pod expansion at all. A shared data store would be preferable. Some of this is time series data, so using a time series database or PostgreSQL plugin could be beneficial. **Extensibility** - The implementations are in different files and the main code is stored in the internal directory, so that helps with extension. There are tests on the core logic and the output matches the expected exactly. Documentation is not something that I've written much of outside of this ReadMe, though there are some comments both by me and the AI. Extending the models depends on what you want to accomplish, such as if you want more generality like re-implementing Prometheus/Grafana or something more custom. There are options like putting these values as a metric endpoint, putting them into kafka with a stream aggregator to do whatever compression/aggregation you want, writing explicit models and code for each type, or something generic that leverages Traits/static abstract to have common functions or a module to handle code in the storage. I'd lean away from trying to over-genericize the implementations. **Git History** - The git history of commits on this is fairly clean, so you'll be able to see things like the PriorityQueue. I did commits at major completion checkpoints. **Scaling** - none, this is in-memory only **Deployment** - it's already a docker container with the simulator, so it isn't far from that. **Further AI Usage** - in addition to having the AI walk me through the conversion as an extended learning exercise, I also had it create the docker files. **Performance / Runtime Complexity** - in order to satisfy the metrics used for the return endpoint, a lot of the history is dropped and auto-aggregated. Maps are used to get the history of a device and the data is then appended. `sent_at` on `POST /stats` is ignored. This could allow duplicates, such as if a device implements a retry that didn't actually fail. There's also no concept of a Request ID to ensure idempotency. However, the complexity is O(1) for lookups and since unexpected out of sequence data is dropped, there's no attempt to rectify that something came in the past, as it's assumed to be invalid. This allows for much smaller complexity and simpler logic. ## Running the Simulator To run this project, clone it and be sure to copy the linux simulator into your directory [Linux ARM](https://sy-fleet-interview-assets.s3.us-east-2.amazonaws.com/device-simulator-linux-arm64) [Linux AMD](https://sy-fleet-interview-assets.s3.us-east-2.amazonaws.com/device-simulator-linux-amd64). It's not that I don't trust you guys, but I decided to run the simulator in docker instead of my native system. To run the Go version, run `docker compose up --build`, for the dotnet version, run `docker compose -f docker-compose.dotnet.yml up --build`. The output will be written to the `output` folder at the root.