+14
-10
api/server/feedgetlikes.go
+14
-10
api/server/feedgetlikes.go
···
8
8
"github.com/labstack/echo/v4"
9
9
vyletdatabase "github.com/vylet-app/go/database/proto"
10
10
"github.com/vylet-app/go/generated/vylet"
11
+
"github.com/vylet-app/go/internal/helpers"
11
12
)
12
13
13
14
type GetFeedLikesBySubjectInput struct {
14
15
Uri string `query:"uri"`
15
-
Limit int64 `query:"limit"`
16
+
Limit *int64 `query:"limit"`
16
17
Cursor *string `query:"cursor"`
17
18
}
18
19
19
-
func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, error) {
20
+
func (s *Server) getLikesBySubject(ctx context.Context, subjectUri string, limit int64, cursor *string) ([]*vylet.FeedGetSubjectLikes_Like, *string, error) {
20
21
logger := s.logger.With("name", "getLikesBySubject", "uri", subjectUri)
21
22
22
23
resp, err := s.client.Like.GetLikesBySubject(ctx, &vyletdatabase.GetLikesBySubjectRequest{
···
25
26
Cursor: cursor,
26
27
})
27
28
if err != nil {
28
-
return nil, fmt.Errorf("failed to get likes by subject: %w", err)
29
+
return nil, nil, fmt.Errorf("failed to get likes by subject: %w", err)
29
30
}
30
31
31
32
dids := make([]string, 0, len(resp.Likes))
···
35
36
36
37
profiles, err := s.getProfiles(ctx, dids)
37
38
if err != nil {
38
-
return nil, fmt.Errorf("failed to get profiles for subject: %w", err)
39
+
return nil, nil, fmt.Errorf("failed to get profiles for subject: %w", err)
39
40
}
40
41
41
42
likes := make([]*vylet.FeedGetSubjectLikes_Like, 0, len(resp.Likes))
···
53
54
})
54
55
}
55
56
56
-
return likes, nil
57
+
return likes, resp.Cursor, nil
57
58
}
58
59
59
-
func (s *Server) handleGetLikesBySubject(e echo.Context) error {
60
+
func (s *Server) handleGetSubjectLikes(e echo.Context) error {
60
61
ctx := e.Request().Context()
61
62
62
-
logger := s.logger.With("name", "handleGetLikesByPost")
63
+
logger := s.logger.With("name", "handleGetSubjectLikes")
63
64
64
65
var input GetFeedLikesBySubjectInput
65
66
if err := e.Bind(&input); err != nil {
···
71
72
return NewValidationError("uri", "URI must be provided")
72
73
}
73
74
74
-
if input.Limit < 1 || input.Limit > 100 {
75
+
if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) {
75
76
return NewValidationError("limit", "limit must be between 1 and 100")
77
+
} else if input.Limit == nil {
78
+
input.Limit = helpers.ToInt64Ptr(25)
76
79
}
77
80
78
81
logger = logger.With("uri", input.Uri)
79
82
80
-
likes, err := s.getLikesBySubject(ctx, input.Uri, input.Limit, input.Cursor)
83
+
likes, cursor, err := s.getLikesBySubject(ctx, input.Uri, *input.Limit, input.Cursor)
81
84
if err != nil {
82
85
logger.Error("failed to get subject likes", "err", err)
83
86
return ErrInternalServerErr
84
87
}
85
88
86
89
return e.JSON(200, vylet.FeedGetSubjectLikes_Output{
87
-
Likes: likes,
90
+
Likes: likes,
91
+
Cursor: cursor,
88
92
})
89
93
}
+87
-9
api/server/feedgetposts.go
+87
-9
api/server/feedgetposts.go
···
3
3
import (
4
4
"context"
5
5
"encoding/json"
6
+
"errors"
6
7
"fmt"
8
+
"sort"
7
9
"time"
8
10
9
11
"github.com/bluesky-social/indigo/lex/util"
···
14
16
"github.com/vylet-app/go/internal/helpers"
15
17
"golang.org/x/sync/errgroup"
16
18
)
17
-
18
-
type GetFeedPostsInput struct {
19
-
Uris []string `query:"uris"`
20
-
}
21
19
22
20
func (s *Server) getPosts(ctx context.Context, uris []string) (map[string]*vylet.FeedPost, error) {
23
21
resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{
···
82
80
}
83
81
84
82
func (s *Server) getPostViews(ctx context.Context, uris []string, viewer string) (map[string]*vylet.FeedDefs_PostView, error) {
85
-
logger := s.logger.With("name", "feedPostsToPostViews")
86
-
87
83
resp, err := s.client.Post.GetPosts(ctx, &vyletdatabase.GetPostsRequest{
88
84
Uris: uris,
89
85
})
···
94
90
return nil, ErrDatabaseNotFound
95
91
}
96
92
97
-
dids := make([]string, 0, len(resp.Posts))
93
+
feedPostViews, err := s.postsToPostViews(ctx, resp.Posts, viewer)
94
+
if err != nil {
95
+
return nil, err
96
+
}
97
+
98
+
return feedPostViews, nil
99
+
}
100
+
101
+
func (s *Server) postsToPostViews(ctx context.Context, posts map[string]*vyletdatabase.Post, viewer string) (map[string]*vylet.FeedDefs_PostView, error) {
102
+
logger := s.logger.With("name", "postsToPostViews")
103
+
104
+
uris := make([]string, 0, len(posts))
105
+
dids := make([]string, 0, len(posts))
98
106
addedDids := make(map[string]struct{})
99
-
for _, post := range resp.Posts {
107
+
for uri, post := range posts {
108
+
uris = append(uris, uri)
109
+
100
110
if _, ok := addedDids[post.AuthorDid]; ok {
101
111
continue
102
112
}
···
131
141
}
132
142
133
143
feedPostViews := make(map[string]*vylet.FeedDefs_PostView)
134
-
for _, post := range resp.Posts {
144
+
for _, post := range posts {
135
145
profileBasic, ok := profiles[post.AuthorDid]
136
146
if !ok {
137
147
logger.Warn("failed to get profile for post", "did", post.AuthorDid, "uri", post.Uri)
···
197
207
return feedPostViews, nil
198
208
}
199
209
210
+
type GetFeedPostsInput struct {
211
+
Uris []string `query:"uris"`
212
+
}
213
+
200
214
func (s *Server) handleGetPosts(e echo.Context) error {
201
215
ctx := e.Request().Context()
202
216
···
245
259
Posts: orderedPostViews,
246
260
})
247
261
}
262
+
263
+
type GetFeedActorPostsInput struct {
264
+
Actor string `query:"actor"`
265
+
Limit *int64 `query:"limit"`
266
+
Cursor *string `query:"cursor"`
267
+
}
268
+
269
+
func (s *Server) handleGetActorPosts(e echo.Context) error {
270
+
ctx := e.Request().Context()
271
+
272
+
logger := s.logger.With("name", "handleGetActorPosts")
273
+
274
+
var input GetFeedActorPostsInput
275
+
if err := e.Bind(&input); err != nil {
276
+
logger.Error("failed to bind", "err", err)
277
+
return ErrInternalServerErr
278
+
}
279
+
280
+
if input.Limit != nil && (*input.Limit < 1 || *input.Limit > 100) {
281
+
return NewValidationError("limit", "limit must be between 1 and 100")
282
+
} else if input.Limit == nil {
283
+
input.Limit = helpers.ToInt64Ptr(25)
284
+
}
285
+
286
+
logger = logger.With("actor", input.Actor, "limit", *input.Limit, "cursor", input.Cursor)
287
+
288
+
did, _, err := s.fetchDidHandleFromActor(ctx, input.Actor)
289
+
if err != nil {
290
+
if errors.Is(err, ErrActorNotValid) {
291
+
return NewValidationError("actor", "actor must be a valid DID or handle")
292
+
}
293
+
logger.Error("error fetching did and handle", "err", err)
294
+
return ErrInternalServerErr
295
+
}
296
+
297
+
resp, err := s.client.Post.GetPostsByActor(ctx, &vyletdatabase.GetPostsByActorRequest{
298
+
Did: did,
299
+
Limit: *input.Limit,
300
+
Cursor: input.Cursor,
301
+
})
302
+
if err != nil {
303
+
logger.Error("failed to get posts", "did", did)
304
+
return ErrInternalServerErr
305
+
}
306
+
307
+
postViews, err := s.postsToPostViews(ctx, resp.Posts, "") // TODO: set viewer
308
+
if err != nil {
309
+
s.logger.Error("failed to get post views", "err", err)
310
+
return ErrInternalServerErr
311
+
}
312
+
313
+
sortedPostViews := make([]*vylet.FeedDefs_PostView, 0, len(postViews))
314
+
for _, postView := range postViews {
315
+
sortedPostViews = append(sortedPostViews, postView)
316
+
}
317
+
sort.Slice(sortedPostViews, func(i, j int) bool {
318
+
return sortedPostViews[i].CreatedAt > sortedPostViews[j].CreatedAt
319
+
})
320
+
321
+
return e.JSON(200, vylet.FeedGetActorPosts_Output{
322
+
Posts: sortedPostViews,
323
+
Cursor: resp.Cursor,
324
+
})
325
+
}
+2
-1
api/server/server.go
+2
-1
api/server/server.go
···
129
129
130
130
// app.vylet.feed
131
131
s.echo.GET("/xrpc/app.vylet.feed.getPosts", s.handleGetPosts)
132
-
s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetLikesBySubject)
132
+
s.echo.GET("/xrpc/app.vylet.feed.getSubjectLikes", s.handleGetSubjectLikes)
133
+
s.echo.GET("/xrpc/app.vylet.feed.getActorPosts", s.handleGetActorPosts)
133
134
}
134
135
135
136
func (s *Server) errorHandler(err error, c echo.Context) {
+76
bench.py
+76
bench.py
···
1
+
import requests
2
+
import time
3
+
import sys
4
+
from datetime import datetime
5
+
6
+
# Configuration
7
+
URL = "http://localhost:8080/xrpc/app.vylet.feed.getActorPosts?actor=hailey.at"
8
+
INITIAL_DELAY = 5.0
9
+
MIN_DELAY = 0.001
10
+
RAMP_FACTOR = 0.5
11
+
12
+
13
+
def make_request(session, request_num, delay):
14
+
try:
15
+
start_time = time.time()
16
+
response = session.get(URL, timeout=10)
17
+
elapsed = time.time() - start_time
18
+
19
+
timestamp = datetime.now().strftime("%H:%M:%S")
20
+
rate = 1 / delay if delay > 0 else float("inf")
21
+
22
+
print(
23
+
f"[{timestamp}] Request #{request_num:4d} | "
24
+
f"Status: {response.status_code} | "
25
+
f"Time: {elapsed:.3f}s | "
26
+
f"Rate: {rate:.2f} req/s"
27
+
)
28
+
29
+
return True
30
+
except requests.exceptions.RequestException as e:
31
+
timestamp = datetime.now().strftime("%H:%M:%S")
32
+
print(f"[{timestamp}] Request #{request_num:4d} | ERROR: {e}")
33
+
return False
34
+
35
+
36
+
def main():
37
+
print("=" * 70)
38
+
print("Gradual Load Tester")
39
+
print("=" * 70)
40
+
print(f"Target URL: {URL}")
41
+
print(f"Starting delay: {INITIAL_DELAY}s ({1 / INITIAL_DELAY:.2f} req/s)")
42
+
print(f"Min delay: {MIN_DELAY}s ({1 / MIN_DELAY:.2f} req/s)")
43
+
print(
44
+
f"Ramp factor: {RAMP_FACTOR} (gets {(1 - RAMP_FACTOR) * 100:.0f}% faster each cycle)"
45
+
)
46
+
print("=" * 70)
47
+
print("\nPress Ctrl+C to stop\n")
48
+
49
+
session = requests.Session()
50
+
delay = INITIAL_DELAY
51
+
request_num = 0
52
+
53
+
try:
54
+
while True:
55
+
request_num += 1
56
+
make_request(session, request_num, delay)
57
+
58
+
time.sleep(delay)
59
+
60
+
if delay > MIN_DELAY:
61
+
delay = max(delay * RAMP_FACTOR, MIN_DELAY)
62
+
if delay == MIN_DELAY:
63
+
print(f"\n{'=' * 70}")
64
+
print(f"Reached maximum rate: {1 / MIN_DELAY:.2f} requests/second")
65
+
print(f"{'=' * 70}\n")
66
+
67
+
except KeyboardInterrupt:
68
+
print(f"\n\n{'=' * 70}")
69
+
print(f"Stopped after {request_num} requests")
70
+
print(f"Final rate: {1 / delay:.2f} requests/second")
71
+
print(f"{'=' * 70}")
72
+
sys.exit(0)
73
+
74
+
75
+
if __name__ == "__main__":
76
+
main()
+36
generated/vylet/feedgetActorPosts.go
+36
generated/vylet/feedgetActorPosts.go
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
// Lexicon schema: app.vylet.feed.getActorPosts
4
+
5
+
package vylet
6
+
7
+
import (
8
+
"context"
9
+
10
+
lexutil "github.com/bluesky-social/indigo/lex/util"
11
+
)
12
+
13
+
// FeedGetActorPosts_Output is the output of a app.vylet.feed.getActorPosts call.
14
+
type FeedGetActorPosts_Output struct {
15
+
Cursor *string `json:"cursor,omitempty" cborgen:"cursor,omitempty"`
16
+
Posts []*FeedDefs_PostView `json:"posts" cborgen:"posts"`
17
+
}
18
+
19
+
// FeedGetActorPosts calls the XRPC method "app.vylet.feed.getActorPosts".
20
+
func FeedGetActorPosts(ctx context.Context, c lexutil.LexClient, actor string, cursor string, limit int64) (*FeedGetActorPosts_Output, error) {
21
+
var out FeedGetActorPosts_Output
22
+
23
+
params := map[string]interface{}{}
24
+
params["actor"] = actor
25
+
if cursor != "" {
26
+
params["cursor"] = cursor
27
+
}
28
+
if limit != 0 {
29
+
params["limit"] = limit
30
+
}
31
+
if err := c.LexDo(ctx, lexutil.Query, "", "app.vylet.feed.getActorPosts", params, nil, &out); err != nil {
32
+
return nil, err
33
+
}
34
+
35
+
return &out, nil
36
+
}
+8
internal/helpers/helpers.go
+8
internal/helpers/helpers.go
···
13
13
return &str
14
14
}
15
15
16
+
func ToIntPtr(num int) *int {
17
+
return &num
18
+
}
19
+
20
+
func ToInt64Ptr(num int64) *int64 {
21
+
return &num
22
+
}
23
+
16
24
func ImageCidToCdnUrl(cid string, size string) string {
17
25
return fmt.Sprintf("https://cdn.vylet.app/%s/%s@png", cid, size)
18
26
}