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

Creating stats/ page with reporting showing where all money goes within a closed system. Drafting DDD. #299

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from

Conversation

pwdel
Copy link
Member

@pwdel pwdel commented Sep 7, 2024

  • See tickets.
  • Improvements to testing documentation
  • Created error helper
  • Created way to report stats/ to endpoint at api/v0/stats which reports financial stats for a given instance of SocialPredict.
  • The idea here is that we should be able to calculate and match some given numbers from our state as well, e.g. from wallet ballances. This shows users of the system that nothing, "funny," is going on and that all money created is being accounted for and transparent.
  • If any market makers are added, transactions should show amounts that have been added to those market makers.
  • Likewise for bailouts to individual user accounts, debt limits, etc. Everything should make sense and add up to everyone's aggregate wallet balances. This can be built in the future.
  • Apart from this, created three repositories for data, one for each model, so that we can create an interface between the model and our logic and control how queries happen. Part of my worry was that if we continue writing queries on the fly without a standard logic, we are more likely to create non-performant systems going forward.
  • Likewise, using a repository system could allow us to work with different types of ORM's or databases. If we end up using a repository and refactor this repository usage into our code, it is going to be much easier to switch out how we interact with a database in the future, if that becomes a need.
  • I didn't touch the rest of our repo to keep this MR more controlled, but I did do a test to extract PublicUserInfo with our repository model as a demo. Future refactoring using the repository model could be done on a case by case basis for smaller tickets.

Results of my test on the stats/ api

{"moneyIssued":{"totalMoneyIssued":1000,"debtExtendedToRegularUsers":1000,"additionalDebtExtended":0,"debtExtendedToMarketMakers":0},"revenue":{"totalRevenue":139,"transactionFees":9,"marketCreationFees":130},"expenditures":{"totalExpenditures":0,"bonusesPaid":0,"otherExpenditures":0},"fiscalBalance":139,"totalEquity":139,"liabilities":0}

These amounts are expected. I didn't write a test for this yet.

@pwdel pwdel changed the title Add correction for total money in system per usercount. Creating stats/ page with reporting showing where all money goes within a closed system. Drafting DDD. Sep 9, 2024
@pwdel pwdel linked an issue Sep 9, 2024 that may be closed by this pull request
@pwdel pwdel marked this pull request as ready for review September 19, 2024 18:18
@pwdel pwdel requested review from ajlacey and j4qfrost September 19, 2024 18:18
Copy link
Collaborator

@j4qfrost j4qfrost left a comment

Choose a reason for hiding this comment

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

Left some comments. It looks fine aside from those.

@@ -52,7 +52,11 @@ func ListMarketsHandler(w http.ResponseWriter, r *http.Request) {
marketVolume := marketmath.GetMarketVolume(bets)
lastProbability := probabilityChanges[len(probabilityChanges)-1].Probability

creatorInfo := usersHandlers.GetPublicUserInfo(db, market.CreatorUsername)
creatorInfo, err := usersHandlers.GetPublicUserInfo(db, market.CreatorUsername)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should this actually return a 400 or is it really a 404? Digging into the GetPublicUserInfo function it appears both are possible. For now we can handle this as is, but there should be a TODO or something to bubble up the error and handle it accordingly. Admittedly I don't know what errors GetUserByUsername does return.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, the market must have a creator username associated with it, so if an error is received, what should or process be for handling it? I don't think failing is the right answer here, but I'm not sure what an effective retry policy would be.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this might be a common issue if the creator decides to delete their account. We could create a null user to handle this case or just prevent account deletion unless the user releases the market manually whether by deleting the market or changing "ownership"

Copy link
Member Author

Choose a reason for hiding this comment

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

  1. 404 Error Handling: You are correct that it should be a 404 error when a user is not found, and I will update the logic to reflect that.

  2. Immutable Usernames for Traceability: Usernames should be immutable to maintain traceability of market activities. Allowing users to delete their accounts would remove critical records of the markets they created and how they resolved. Instead, we can implement an archiving system that retains the username but removes sensitive personal information like the display name. This would help prevent users from creating unreliable markets and then disappearing without accountability, ensuring the integrity of the platform.

Put together a decision ticket about this here: https://github.com/orgs/openpredictionmarkets/projects/9/views/1?pane=issue&itemId=80893304

backend/repository/database.go Outdated Show resolved Hide resolved
Joins(query string, args ...interface{}) Database
Scan(dest interface{}) Database
SubQuery() *gorm.DB
Raw(sql string, values ...interface{}) Database
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably remove this if the intention is to limit functionality.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Interfaces should generally be small, this could be addressed by having something like:

type DatabaseModeler interface {
    Model(value interface{}) Database
}

This would then be used anywhere a Model function is needed. This article provides some further reading on why and how this should be done: https://medium.com/@meeusdylan/go-interfaces-keep-them-small-f148ae200d6b

Copy link
Collaborator

Choose a reason for hiding this comment

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

After looking back over this, I'm not sure about how we should be approaching this in general. What benefits do we get from this organization? i.e. what does a Database interface give us overall?

Copy link
Collaborator

Choose a reason for hiding this comment

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

@ajlacey if the intention is to restrict API access to the ORM I understand the intention of this object, but if we just allow public access to the Raw function, then you can run any command on the DB.

Copy link
Collaborator

@ajlacey ajlacey left a comment

Choose a reason for hiding this comment

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

A lot of feedback here, but I think we should go in this order:

  1. Do we need the repository package/database abstraction? If so, what benefits does it give us?
  2. Should the repository package be a separate package or just built on top of the models that are stored in our database type? I think they should be together.
  3. We should look at updating the User type to be composed of its public and private types for convenience.
  4. We should avoid introducing placeholder code. It doesn't give us any functionality and just provides 'dead' code.

Retrospective: I think if we started at 1, we'd be able to decide what that should look like and have an isolated PR just focused on those changes. I think there's also a lot going on here that doesn't appear to be related to creating the stats page (indicative of scope creep). A lot of the new stats code doesn't use the setup config dynamically, which makes it impossible to test the stats code for different scenarios that could arise in the system based on configurations.

## 3. Data Conventions

* The entire application should be as stateless as possible, meaning we have a one-way writeable database of users, markets, bets and calculate all relevant states of the application from as few possible columns within those models.
* We should separate the data logic from the business logic as much as possible with a Domain Driven Design (DDD), meaning we have a repository/ directory which is designed to be the central location to keep functions that extract data from the databases. This should ideally help slow the growth of the codebase over time and keep data extraction more testable, which should make our startless architecture more reliable.
Copy link
Collaborator

Choose a reason for hiding this comment

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

typo stateless/startless:

which should make our startless architecture more reliable.

Comment on lines +12 to +14
if errors.Is(err, gorm.ErrRecordNotFound) {
http.Error(w, "User not found", http.StatusNotFound)
} else {
Copy link
Collaborator

Choose a reason for hiding this comment

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

what if a different record type isn't found? Why do we only use message in one of the cases here?
Do we need a separate error handler?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yea I seconded this in my review.

Comment on lines +40 to +52
if err != nil {
// Check if the error is because the user was not found
if errors.Is(err, gorm.ErrRecordNotFound) {
// User not found, return 404 Not Found
http.Error(w, "User not found", http.StatusNotFound)
} else {
// Internal server error
http.Error(w, "Internal server error", http.StatusInternalServerError)
// Optionally, log the error for debugging purposes
log.Printf("Failed to get user info: %v", err)
}
return
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Note to return to: This feels very similar to the error handler, and I suspect is indicative that the handler isn't necessary.

db.Where("username = ?", username).First(&user)
user, err := repo.GetUserByUsername(username)
if err != nil {
return PublicUserType{}, fmt.Errorf("failed to get user by username: %w", err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of returning an empty struct, which will initialize the default zero values, we should return nil, err.
This prevents us from using values that are not initialized in the event that someone calling this function doesn't check the error.

This change will require the return type to be updated to *PublicUserType

Copy link
Collaborator

Choose a reason for hiding this comment

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

These file names are redundant <type>_repository.go should just be <type>.go

Comment on lines +58 to +63
publicInfo, err := GetPublicUserInfo(db, username)
if err != nil {
helpers.HandleError(w, err, "Error fetching public user info")
return
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

Why does this introduce a new helper to use when the implementation can be done more clearly here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I suspect we can remove this file package, but if not we should rename this package: https://go.dev/blog/package-names#bad-package-names

Comment on lines +57 to +58
http.Error(w, "Can't retrieve user public info.", http.StatusBadRequest)
return
Copy link
Collaborator

Choose a reason for hiding this comment

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

Given the above, if we do return an error here, it should be an internal server error, the request wasn't malformed, the server just failed to retrieve user info.

@@ -52,7 +52,11 @@ func ListMarketsHandler(w http.ResponseWriter, r *http.Request) {
marketVolume := marketmath.GetMarketVolume(bets)
lastProbability := probabilityChanges[len(probabilityChanges)-1].Probability

creatorInfo := usersHandlers.GetPublicUserInfo(db, market.CreatorUsername)
creatorInfo, err := usersHandlers.GetPublicUserInfo(db, market.CreatorUsername)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, the market must have a creator username associated with it, so if an error is received, what should or process be for handling it? I don't think failing is the right answer here, but I'm not sure what an effective retry policy would be.

Copy link
Collaborator

Choose a reason for hiding this comment

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

A lot of these functions have a dependency on the setup config, but don't inject that dependency into their functionality. This needs to be fixed to test these helpers properly.

@j4qfrost
Copy link
Collaborator

Thinking more on this, the PR should be broken down, not just because it touches a lot of files, there are multiple objectives.

@pwdel pwdel mentioned this pull request Sep 24, 2024
@pwdel
Copy link
Member Author

pwdel commented Sep 24, 2024

A lot of feedback here, but I think we should go in this order:

1. Do we need the repository package/database abstraction? If so, what benefits does it give us?

2. Should the `repository` package be a separate package or just built on top of the models that are stored in our database type? I think they should be together.

3. We should look at updating the `User` type to be composed of its public and private types for convenience.

4. We should avoid introducing placeholder code. It doesn't give us any functionality and just provides 'dead' code.

Retrospective: I think if we started at 1, we'd be able to decide what that should look like and have an isolated PR just focused on those changes. I think there's also a lot going on here that doesn't appear to be related to creating the stats page (indicative of scope creep). A lot of the new stats code doesn't use the setup config dynamically, which makes it impossible to test the stats code for different scenarios that could arise in the system based on configurations.

Put together a ticket on point 1. Other points are separate because I'll remove the extra, non-used functions and we could take care of Public/Private user model in a separate ticket. In fact I'll put another ticket together for that right now as well.

@pwdel pwdel marked this pull request as draft October 2, 2024 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants