Explorar o código

Push a fresh account image as soon as a Triumph is claimed

A claim wrote its flag into the claim store, but nothing told the peer to read the account again,
so the Triumph only turned Acquired once some unrelated action happened to stage a Family-4
refresh. Dismantling an item worked; claiming alone did not.

Send the account image with the claim. A claim needs no transaction: it has already written the
store, it changes account object bytes rather than the manifest, and there is nothing to commit
afterwards. So it travels as a flag on the outcome and picks up
append_account_resync_notification, which is exactly one full account snapshot at the peer's next
Family-4 version. A snapshot that cannot be built is logged and the ladder is left alone, matching
how the change-character path treats the same failure.

Report where the record extraction stops. Without it a failed extraction looked like a broken
claim three layers downstream: the domain stayed empty, every claim failed on record_definition,
and nothing said why.

Measured: claiming record 221 resolves flag index 9144 and a 179-object family-4 resync leaves
4 ms later, and the Triumph turns Acquired with no second action.

Known gap: the records domain is not written to build_data.bin, so it is only populated on a
cache miss. On a warm cache the extraction never runs and claims fail on record_definition.
Millie hai 3 semanas
pai
achega
155dc4b264

+ 19 - 0
Sunrise/src/client/content/items/packages/package_record_build.cpp

@@ -1,11 +1,27 @@
+#include <array>
+#include <cstdio>
 #include <cstring>
 #include <cstring>
 
 
+#include "../../../../core/logging/log.h"
+
 #include "../../../../state/build_data/runtime.h"
 #include "../../../../state/build_data/runtime.h"
 #include "internal.h"
 #include "internal.h"
 
 
 namespace sunrise::client::content::items::packages {
 namespace sunrise::client::content::items::packages {
 namespace {
 namespace {
 
 
+/** Reports where the record pass stopped, so a silent miss cannot look like a working claim. */
+void report(const char* stage, unsigned long long detail) noexcept {
+    std::array<char, 128> line{};
+    const int count = std::snprintf(
+        line.data(), line.size(), "ev=pkg stage=records result=%s detail=%llu", stage, detail);
+    if (count > 0) {
+        core::log::write(core::log::Channel::client,
+                         core::log::Level::info,
+                         {line.data(), static_cast<std::size_t>(count)});
+    }
+}
+
 /** A record with no completion flag carries a non-positive slot, which addresses nothing. */
 /** A record with no completion flag carries a non-positive slot, which addresses nothing. */
 [[nodiscard]] constexpr bool addressable_slot(std::int16_t slot) noexcept {
 [[nodiscard]] constexpr bool addressable_slot(std::int16_t slot) noexcept {
     return slot > 0;
     return slot > 0;
@@ -43,6 +59,7 @@ bool build_records(const reader::Source& source,
         || mapRows.dataOffset
         || mapRows.dataOffset
                    + static_cast<std::size_t>(mapRows.count) * tables::kUnlockMapRowStride
                    + static_cast<std::size_t>(mapRows.count) * tables::kUnlockMapRowStride
                > blob.size()) {
                > blob.size()) {
+        report("flag_map_fail", mapTag);
         return false;
         return false;
     }
     }
 
 
@@ -76,6 +93,7 @@ bool build_records(const reader::Source& source,
         || rows.count == 0 || rows.count > output.size()
         || rows.count == 0 || rows.count > output.size()
         || rows.dataOffset + static_cast<std::size_t>(rows.count) * tables::kRecordRowStride
         || rows.dataOffset + static_cast<std::size_t>(rows.count) * tables::kRecordRowStride
                > blob.size()) {
                > blob.size()) {
+        report("record_table_fail", tableTag);
         return false;
         return false;
     }
     }
 
 
@@ -94,6 +112,7 @@ bool build_records(const reader::Source& source,
         }
         }
         ++count;
         ++count;
     }
     }
+    report("ok", static_cast<unsigned long long>(count));
     return count != 0;
     return count != 0;
 }
 }
 
 

+ 1 - 0
Sunrise/src/server/bap/encrypted/body/bap_service_body.cpp

@@ -198,6 +198,7 @@ bool process(const ServiceRoute& route,
             return false;
             return false;
         }
         }
         outcome.hasSubscription = webOutcome.hasSubscription;
         outcome.hasSubscription = webOutcome.hasSubscription;
+        outcome.hasRecordClaim = webOutcome.hasRecordClaim;
         outcome.subscription = webOutcome.subscription;
         outcome.subscription = webOutcome.subscription;
         const auto* equipmentSwap =
         const auto* equipmentSwap =
             web_service::mutation_if<state::PendingEquipmentSwap>(webOutcome);
             web_service::mutation_if<state::PendingEquipmentSwap>(webOutcome);

+ 2 - 0
Sunrise/src/server/bap/encrypted/internal.h

@@ -79,6 +79,8 @@ struct ItemDismantleTransaction {
 /** Optional side effect produced while decoding one authenticated service body. */
 /** Optional side effect produced while decoding one authenticated service body. */
 struct ServiceOutcome {
 struct ServiceOutcome {
     bool hasSubscription{};
     bool hasSubscription{};
+    /** A Triumph claim changed the account flag bank and its image has to follow. */
+    bool hasRecordClaim{};
     middleware::queuez::Subscription subscription{};
     middleware::queuez::Subscription subscription{};
     bool hasUnsubscription{};
     bool hasUnsubscription{};
     middleware::bap::family_unsubscription::Request unsubscription{};
     middleware::bap::family_unsubscription::Request unsubscription{};

+ 10 - 0
Sunrise/src/server/bap/encrypted/queuez/queuez_outcome_staging.cpp

@@ -369,6 +369,16 @@ bool stage_service_outcome(Scratch& scratch,
         }
         }
         middleware::secure_channel::advance_nonce(nonce);
         middleware::secure_channel::advance_nonce(nonce);
         after = acquisition.after;
         after = acquisition.after;
+    } else if (outcome.hasRecordClaim) {
+        // A claim rewrites one byte of the account flag bank and leaves the manifest alone, so a
+        // full account snapshot at the next version carries it with no other staging.
+        if (!push::append_account_resync_notification(
+                scratch, before, key, nonce, response, written, after)) {
+            core::log::write(core::log::Channel::server,
+                             core::log::Level::warn,
+                             "ev=ws1801 stage=queuez_resync result=fail");
+            return true;
+        }
     } else if (itemDismantle != nullptr) {
     } else if (itemDismantle != nullptr) {
         // A dismantle removes exactly one resident while preserving the relative order of every
         // A dismantle removes exactly one resident while preserving the relative order of every
         // survivor. The character after-image and empty release descriptor must fit together or
         // survivor. The character after-image and empty release descriptor must fit together or

+ 2 - 1
Sunrise/src/server/web_service/web_service_actions.cpp

@@ -849,7 +849,6 @@ void claim_record(const middleware::web_service::Message& message, Outcome& outc
     // Deliberately no mutation. The shared reply path answers an action that prepares nothing with
     // Deliberately no mutation. The shared reply path answers an action that prepares nothing with
     // the refusal status, so preparing a placeholder here would turn a silently-accepted claim into
     // the refusal status, so preparing a placeholder here would turn a silently-accepted claim into
     // an explicitly rejected one. Attach the transition here once claimed state is identified.
     // an explicitly rejected one. Attach the transition here once claimed state is identified.
-    (void)outcome;
     namespace records = state::build_data::records;
     namespace records = state::build_data::records;
     middleware::web_service::messages::opcode1801::Request request{};
     middleware::web_service::messages::opcode1801::Request request{};
     if (!middleware::web_service::messages::opcode1801::parse_request(message, request)) {
     if (!middleware::web_service::messages::opcode1801::parse_request(message, request)) {
@@ -878,6 +877,8 @@ void claim_record(const middleware::web_service::Message& message, Outcome& outc
                             definition.completionFlagIndex);
                             definition.completionFlagIndex);
         return;
         return;
     }
     }
+    // The claim is already in the store, so the account image only has to be sent again.
+    outcome.hasRecordClaim = true;
     report_record_claim(
     report_record_claim(
         message, "ok", "claimed", request.recordIndex, definition.completionFlagIndex);
         message, "ok", "claimed", request.recordIndex, definition.completionFlagIndex);
 }
 }

+ 2 - 0
Sunrise/src/server/web_service/web_service_runtime.h

@@ -14,6 +14,8 @@ namespace sunrise::server::web_service {
 struct Outcome {
 struct Outcome {
     bool hasSubscription{};
     bool hasSubscription{};
     middleware::queuez::Subscription subscription{};
     middleware::queuez::Subscription subscription{};
+    /** A claim changed the account flag bank, so a fresh account image has to follow. */
+    bool hasRecordClaim{};
     /** An opcode-504 pick moved the selection and its Family-4 object still has to follow. */
     /** An opcode-504 pick moved the selection and its Family-4 object still has to follow. */
     bool hasSelectedCharacter{};
     bool hasSelectedCharacter{};
     std::uint64_t selectedCharacterSoid{};
     std::uint64_t selectedCharacterSoid{};