GithubHelp home page GithubHelp logo

Comments (20)

MrIvanPlays avatar MrIvanPlays commented on August 15, 2024 2

you both present fair points. idk who but someone suggested that we shall use some kind of response object (for idk what reason, possibly as I think of it - to differentiate unexpected exceptions from expected exceptions), and in order to handle it properly, you need to do something like this:

// this is the current implementation we have
account.hasPermissions(...).whenComplete((res, e) -> {
  if (!res.isSuccessful()) {
    sender.sendMessage(res.getFailureReason().getDescription());
    return;
  }
  if (e != null) {
    // do whatever
    return;
  }
  
  // do whatever with res.getResult() here
});

however, if we ditch Response and FailureReason and create a RuntimeException without a stacktrace, it would look more like this:

account.hasPermissions(...).whenComplete((triStateResult, e) -> {
  if (e != null) {
    if (e instanceof TreasuryException) { // name under debate, this is for the example
      sender.sendMessage(e.getMessage());
      return;
    }
    // do something else with the other exceptions
    return;
  }
  // do whatever with triStateResult here
});

so what i propose is to create a RuntimeException that doesn't have a stacktrace that shall be used instead of Response and FailureReason.

from treasury.

Jikoo avatar Jikoo commented on August 15, 2024 1

I thought part of the point of swapping to CF over a subscriber was to swap to using CF exception handling instead of baking in our own. This seems more like a question of "is the Response class useful or is it a redundant wrapper when we could be using CF exception handling?"

In my mind, unexpected exceptions should not be included in our intended pipeline. They indicate a problem with the code, so the code should be fixed instead of the exception being handled.

from treasury.

MrIvanPlays avatar MrIvanPlays commented on August 15, 2024 1

Well response does serve a role because sending a failure reason downstream is much more lighter than an exception. Also that this failure reason can be localized and directly sent to the player. As I said, expected exceptions when working with a database should fallback to a failure response, other exceptions are afaik already caught by CFs.

from treasury.

MrIvanPlays avatar MrIvanPlays commented on August 15, 2024 1

Otherwise, it may be a good idea to make FailureReasons accept a map of locales to descriptions .

no just add a Locale argument to getDescription

however you make an exception it is still heavy. just look at how much exploits bungeecord has because of it's usage of CancelSendSignal inside the packet handlers. that's how you'll understand how heavy they are.

from treasury.

Jikoo avatar Jikoo commented on August 15, 2024 1

however you make an exception it is still heavy. just look at how much exploits bungeecord has because of it's usage of CancelSendSignal inside the packet handlers. that's how you'll understand how heavy they are.

I have already posted benchmarks of lightweight "informative" exception handling in previous discussions: #37 (comment)
On modern Java an exception without a trace for information purposes performs very marginally slower than non-exceptional code. It's really not a concern. I get that we still support Java 8, but given that its market share is slipping even more, I don't know that increasing the complexity of error handling when using the API is worth is worth it. The CF already includes all of the error handling, so it's not like we're gaining a performance boost by not throwing any exceptions.

That said, we aren't using CFs because they have error handling built in. We're using them because they clearly signal to users that the results of the call are not necessarily available immediately and the code should not be run synchronously. In that case, perhaps handling errors using the CF instead of our constructs is bad practice? On the user side not handling errors via CF is much easier to remember. Any uncaught exceptions are then a code issue with either the CF provider or the inputs by the user, not potentially informative results.

I just want to be cautious with our approach to this because this is an area where we'll cause everyone a lot of headache if we don't have a firm stance on correct practice.

from treasury.

MrIvanPlays avatar MrIvanPlays commented on August 15, 2024 1

I think the current stuff as-is is already good because it's a clear signal about whether the request failed or didn't.

from treasury.

Jikoo avatar Jikoo commented on August 15, 2024 1

Apologies, I thought I had replied here. That seems good to me as well, yeah. Flow all the way through, handle expected failure errors once when handling the expected result. Clean and simple.

from treasury.

Jikoo avatar Jikoo commented on August 15, 2024 1

That would work. Throwable does have #getLocalizedMessage, so theoretically it is already possible to do to some degree with Transactions, but expanding the capacity and standardizing would be good.

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

I thought part of the point of swapping to CF over a subscriber was to swap to using CF exception handling instead of baking in our own. This seems more like a question of "is the Response class useful or is it a redundant wrapper when we could be using CF exception handling?"

What do you think, @MrIvanPlays? Since we've removed the standard list of failure reasons (a decision which I still agree with), I'm unsure if Response serves any use anymore.

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

As we are working with CFs, I am not at all phased by whichever minute overhead exceptions will bring.

Localization is a great point, though note that failure reasons are not localized through the API itself. If we did go down the Exceptions path, we could always make a LocalizedException class which would allow for per-locale descriptions on what happened. Otherwise, it may be a good idea to make FailureReasons accept a map of locales to descriptions .

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

Otherwise, it may be a good idea to make FailureReasons accept a map of locales to descriptions .

no just add a Locale argument to getDescription

however you make an exception it is still heavy. just look at how much exploits bungeecord has because of it's usage of CancelSendSignal inside the packet handlers. that's how you'll understand how heavy they are.

exceptions in packets are a horrible horrible idea. in workloads which are already heavy, they would contribute next to nothing.

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

I do like how responses make it easy for implementations to report human-readable issues to users, e.g., account can't afford transaction.

However, I don't like how it is creating an entire extra layer that everyone has to operate with the API.


What would prevent us from just using Exceptions - but - make a special Exception class (which extends RuntimeException) that is intended to be thrown for generic failures, specifically those which are used for FailureReason?

Implementations would then be encouraged to deal with such an exception differently (use instanceof to check it), i.e., print the user friendly message to the user, rather than say an 'error occurred, inform an admin'.

This means we no longer have to deal with two separate error handling systems, just one now. Cleaner code, easier usage, less confusion.


On @Jikoo's concern about people not remembering to handle errors in CFs - In our documentation, we can make it very clear that error handling must be done with the CFs. If people are not able to use CFs, then that is their problem, it's part of the Java STL, it's widely documented.

from treasury.

Jikoo avatar Jikoo commented on August 15, 2024

What would prevent us from just using Exceptions - but - make a special Exception class (which extends RuntimeException) that is intended to be thrown for generic failures, specifically those which are used for FailureReason?

Implementations would then be encouraged to deal with such an exception differently (use instanceof to check it), i.e., print the user friendly message to the user, rather than say an 'error occurred, inform an admin'.

This means we no longer have to deal with two separate error handling systems, just one now. Cleaner code, easier usage, less confusion.

In my mind this was the original idea for using CFs. A lot of the arguments for and against this are basically rehashes of the discussion we already had about ease of use for beginner programmers.

After #161, I was under the impression that 2.0 was going to be focused on providing an API for advanced users, supplying more documentation and examples to help beginners instead of catering to them. Responses don't really hit that mark for me.
If we are saying "the Subscriber model was good for helping new users but had potential needless overhead" then I agree that the Response model is good. The only benefit is that advanced users will not be wrapping what was (possibly, depending on implementation) internally a CF in another CF.

Here's a Response adaptation of #161's examples:

Response adaptation
public void setBalance(
    CommandSender sender, UUID targetPlayerId, BigDecimal amount, Currency currency
) {
  EconomyTransactionInitiator<?> initiator;
  if (sender instanceof Player) {
    initiator = EconomyTransactionInitiator.createInitiator(EconomyTransactionInitiator.Type.PLAYER,
        ((Player) sender).getUniqueId()
    );
  } else {
    initiator = EconomyTransactionInitiator.SERVER;
  }

  economy.hasAccount(AccountData.forPlayerAccount(targetPlayerId))
      .thenCompose(response -> {
        if (response.isSuccessful() && response.getResult() == TriState.TRUE) {
          return economy.accountAccessor().player().withUniqueId(targetPlayerId).get();
        } else {
          return CompletableFuture.failedFuture(new FailureException(response.getFailureReason()));
        }
      })
      .thenCompose(response -> {
        if (response.isSuccessful()) {
          return response.getResult().doTransaction(
              EconomyTransaction
                  .newBuilder()
                  .withCurrency(currency)
                  .withInitiator(initiator)
                  .withTransactionAmount(amount)
                  .withTransactionType(EconomyTransactionType.SET)
                  .build()
          );
        } else {
          return CompletableFuture.failedFuture(new FailureException(response.getFailureReason()));
        }
      })
      .thenCompose(response -> {
        if (response.isSuccessful()) {
          return CompletableFuture.completedFuture(response.getSuccess());
        } else {
          return CompletableFuture.failedFuture(new FailureException(response.getFailureReason()));
      })
      .whenComplete((bal, error) -> {
        if (error != null) {
          sender.sendMessage("Something went wrong!");
          return;
        }

        sender.sendMessage("Success! New balance is " + bal.doubleValue() + "!");
      });
}

This is actually less succinct than using EconomySubscriber#asFuture - this is (ignoring an additional 7 lines from the fact that the original used a set balance, which we've removed direct access to for good reasons) 43 lines where using CFs for the Subscriber model was 32.

This can be simplified with a method that converts a Response into a CF:

public static <T> CompletableFuture<T> asFuture(Response<T> response) {
  if (response.isSuccessful()) {
    return CompletableFuture.completedFuture(response.getResult());
  }
  return CompletableFuture.failedFuture(new FailureException(response.getFailureReason()));
}

The down side is that this approach adds a .thenCompose(Response::asFuture) on every call, which seems annoying long-term, just like constantly using EconomySubscriber#asFuture was.
This results in a matching 32 (+7) lines:

Response with `Response::asFuture`
public void setBalance(
    CommandSender sender, UUID targetPlayerId, BigDecimal amount, Currency currency
    ) {
  EconomyTransactionInitiator<?> initiator;
  if (sender instanceof Player) {
    initiator = EconomyTransactionInitiator.createInitiator(EconomyTransactionInitiator.Type.PLAYER,
        ((Player) sender).getUniqueId()
    );
  } else {
    initiator = EconomyTransactionInitiator.SERVER;
  }

  economy.hasAccount(AccountData.forPlayerAccount(targetPlayerId))
      .thenCompose(Response::asFuture)
      .thenCompose(val -> {
        if (val == TriState.TRUE) {
          return response.getResult().doTransaction(
              EconomyTransaction
                  .newBuilder()
                  .withCurrency(currency)
                  .withInitiator(initiator)
                  .withTransactionAmount(amount)
                  .withTransactionType(EconomyTransactionType.SET)
                  .build()
          );
        } else {
          return CompletableFuture.failedFuture(new FailureException("no account"));
        }
      })
      .thenCompose(Response::asFuture)
      .whenComplete((bal, error) -> {
        if (error != null) {
          sender.sendMessage("Something went wrong!");
          return;
        }

        sender.sendMessage("Success! New balance is " + bal.doubleValue() + "!");
      });
}
Apologies about any errors, I wrote all this code in the editor like a goof rather than load up my IDE

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

[...] help beginners instead of catering to them. Responses don't really hit that mark for me.

The API isn't purposely being catered towards any particular audience. It has and always will be suited towards more advanced users than Vault. This is because we have decided that we don't want to cater towards synchronous usage of methods which were designed to be used concurrently (- a decision I still agree with).

the Subscriber model was good for helping new users but had potential needless overhead

The adoption of the Subscriber model is something I campaigned for, and looking back, it's clearly a major flaw of the v1 APIs. My opinion on the Subscriber model is that it's inferior to CFs in many ways, and in particular, its verbosity and overall syntax made it more difficult to use than CFs. There were several other advantages discussed in the thread which concluded in v2 making a full pivot to CFs.

Lately I have been implementing Treasury v2 into my own economy provider, and I am relieved that we have switched to CFs. It's basically a testing ground for the APIs before we release them, I have found several issues doing so, most of which would have otherwise required breaking changes to patch (which won't be an option on our table upon v2's release).

Response - CF code

I'm not convinced that responses are required at all. Here's my opinion -

Advantages:

  • Makes it easy for plugins to communicate basic errors, such as 'account can't afford this transaction'. These are distinguished from code/backend failures such as the database not being responsive.

Disadvantages:

  • To counter the only advantage I acknowledge, this can also be done via exceptions, which I've explained here.

  • CFs have inbuilt error handling. Wrapping something in a Result and then a CF is a double-layer of error handling which is unnecessary and increases code verbosity.

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

so what i propose is to create a RuntimeException that doesn't have a stacktrace that shall be used instead of Response and FailureReason.

^^^^^ Yes! This is exactly what I am looking for. @Jikoo, what do you think?

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

@Jikoo, if you're available, please see above. Thanks. :)

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

Thanks Jikoo!

It's interesting to note how we were originally using exceptions, and transitioned into Responses. See: #29

I would like to review the discussions on that Spigot thread before I confirm this.

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

On mobile and about to sleep so can't check. Are we able to make the treasury exception offer multilingual exception description messages? Otherwise they'll all be English which is probably undesirable.

from treasury.

Jikoo avatar Jikoo commented on August 15, 2024

If they're Exceptions they could be translated via ResourceBundle if we add a constructor accepting one, though I'll admit I'm not sure what Java version added that. Other than that it would be the economy provider's responsibility to provide translatability for their exceptions. That does unfortunately mean that they would be providing server-wide set translations rather than individualized translations. That was the intent behind the FailureReason initially - give API users a fixed set of cases to translate off of.

from treasury.

lokka30 avatar lokka30 commented on August 15, 2024

Unfortunately it was difficult to standardise with so many different use cases. Potentially treasury exceptions could have a method which gets the failure message with a locale argument?

from treasury.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.