fdbw commited on
Commit
d2b7d75
·
verified ·
1 Parent(s): 223b6b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -58
app.py CHANGED
@@ -2,60 +2,78 @@ import os
2
  import threading
3
  import traceback
4
  import sys
5
- import fcntl
6
 
7
- # ====================== 核心:原子性文件锁(操作系统级别,绝对单次执行) ======================
8
- def execute_only_once(func):
9
- """装饰器:通过原子性文件锁,确保函数只执行一次(无视Python线程/模块隔离)"""
10
- # 锁文件路径(容器内选/tmp,确保可写)
11
- LOCK_FILE = "/tmp/photo_show_atomic.lock"
12
- # 标记文件:记录是否执行成功
13
- MARK_FILE = "/tmp/photo_show_done.mark"
14
 
15
- def wrapper(*args, **kwargs):
16
- # 第一步:先判断是否已执行成功(快速返回,避免重复操作锁文件)
17
- if os.path.exists(MARK_FILE):
18
- print(f"【跳过】线程{threading.current_thread().ident}:函数已执行成功,直接返回")
19
- return
 
 
 
 
20
 
21
- # 第二步:原子性创建锁文件(操作系统级别的原子操作,唯一能保证单次执行)
 
 
22
  try:
23
- # O_CREAT:创建文件;O_EXCL:如果文件已存在,抛出异常;O_WRONLY:只写
24
- fd = os.open(LOCK_FILE, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
25
- # 写入当前线程/进程信息(可选,用于排查)
26
- os.write(fd, f"pid:{os.getpid()}, tid:{threading.current_thread().ident}".encode())
27
- os.close(fd)
28
- except FileExistsError:
29
- # 锁文件已存在,说明已有线程在执行,直接返回
30
- print(f"【跳过】线程{threading.current_thread().ident}:已有线程在执行,返回")
31
- return
32
 
33
- # 第三步:执行目标函数(只有拿到锁的线程能走到这里)
34
- try:
35
- result = func(*args, **kwargs)
36
- # 执行成功,创建标记文件
37
- with open(MARK_FILE, "w") as f:
38
- f.write("done")
39
- return result
40
- except Exception as e:
41
- print(f"【错误】函数执行失败:{e}")
42
- # 执行失败,删除锁文件,允许下次执行
43
- if os.path.exists(LOCK_FILE):
44
- os.remove(LOCK_FILE)
45
- raise e
46
- finally:
47
- # 可选:执行完成后删除锁文件(若需要下次启动重新执行,取消注释)
48
- # if os.path.exists(LOCK_FILE):
49
- # os.remove(LOCK_FILE)
50
- pass
51
 
52
- return wrapper
 
 
 
53
 
54
- # ====================== 核心功能函数(用原子锁装饰器包裹) ======================
55
- @execute_only_once
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  def show_photos():
57
  # ========== 上下文日志(保留,用于排查) ==========
58
- print(f"\n【上下文】进程ID:{os.getpid()},线程ID:{threading.current_thread().ident}")
59
  print(f"【调用栈】:{''.join(traceback.format_stack())}")
60
  # ========== 原有业务逻辑 ==========
61
  photos = ["廖荣全.jpg"]
@@ -63,20 +81,19 @@ def show_photos():
63
  for photo in photos:
64
  print(f"已显示: {photo}")
65
 
66
- # ====================== 关键:用__main__守护 + 强制清理残留锁(可选) ======================
67
- if __name__ == '__main__':
68
- # 可选:容器重启时,若有残留的标记文件,可根据需求删除(比如每次启动都要执行一次)
69
- # MARK_FILE = "/tmp/photo_show_done.mark"
70
- # LOCK_FILE = "/tmp/photo_show_atomic.lock"
71
- # if os.path.exists(MARK_FILE):
72
- # os.remove(MARK_FILE)
73
- # if os.path.exists(LOCK_FILE):
74
- # os.remove(LOCK_FILE)
75
 
 
 
76
  # 只执行一次调用
77
  show_photos()
78
 
79
- # ========== 可选:打印所有线程信息(定位根因) ==========
80
- print("\n【所有线程详情(根因定位)】:")
81
- for thread in threading.enumerate():
82
- print(f"线程名:{thread.name},ID:{thread.ident},守护线程:{thread.daemon}")
 
 
 
2
  import threading
3
  import traceback
4
  import sys
 
5
 
6
+ # ====================== 核心配置:持久化计数文件(放在/app目录,非/tmp) ======================
7
+ # 选择/app目录(代码目录,容器内一般可写,且不会被临时清理)
8
+ COUNT_FILE = "/app/photo_show_count.txt"
9
+ # 最大执行次数:只允许执行1次
10
+ MAX_EXECUTE_TIMES = 1
 
 
11
 
12
+ # ====================== 原子性读写计数(解决多线程/多MainThread的计数竞争) ======================
13
+ def get_and_increment_count():
14
+ """原子性读取并增加执行计数,返回当前计数"""
15
+ # 初始化计数文件(若不存在)
16
+ if not os.path.exists(COUNT_FILE):
17
+ with open(COUNT_FILE, "w") as f:
18
+ f.write("0")
19
+ # 赋予读写权限(防止容器权限问题)
20
+ os.chmod(COUNT_FILE, 0o644)
21
 
22
+ # 原子性操作:用文件锁包裹读写(防止两个MainThread同时读写)
23
+ with open(COUNT_FILE, "r+") as f:
24
+ # 加文件锁(Windows用msvcrt,Linux用fcntl,这里兼容Linux容器)
25
  try:
26
+ import fcntl
27
+ fcntl.flock(f, fcntl.LOCK_EX)
28
+ except ImportError:
29
+ # Windows环境备用(若需要)
30
+ import msvcrt
31
+ msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1024)
 
 
 
32
 
33
+ # 读取当前计数
34
+ current_count = int(f.read().strip())
35
+ # 移动文件指针到开头,写入新计数
36
+ f.seek(0)
37
+ f.write(str(current_count + 1))
38
+ f.truncate() # 截断多余内容
39
+
40
+ # 释放锁(with块结束后自动释放)
41
+ return current_count + 1
42
+
43
+ # ====================== 装饰器:限制执行次数 ======================
44
+ def limit_execute_times(max_times):
45
+ def decorator(func):
46
+ def wrapper(*args, **kwargs):
47
+ # 获取当前执行次数
48
+ current_count = get_and_increment_count()
49
+ print(f"【执行计数】当前是第{current_count}次执行,最大允许{max_times}次")
 
50
 
51
+ # 超过次数则返回
52
+ if current_count > max_times:
53
+ print(f"【跳过】线程{threading.current_thread().ident}({threading.current_thread().name}):已达到最大执行次数,返回")
54
+ return None
55
 
56
+ # 未超过次数则执行函数
57
+ try:
58
+ return func(*args, **kwargs)
59
+ except Exception as e:
60
+ print(f"【错误】执行失败:{e}")
61
+ # 执行失败:回退计数(可选,根据需求)
62
+ with open(COUNT_FILE, "r+") as f:
63
+ import fcntl
64
+ fcntl.flock(f, fcntl.LOCK_EX)
65
+ f.seek(0)
66
+ f.write(str(current_count - 1))
67
+ f.truncate()
68
+ raise e
69
+ return wrapper
70
+ return decorator
71
+
72
+ # ====================== 核心功能函数(限制执行次数为1次) ======================
73
+ @limit_execute_times(MAX_EXECUTE_TIMES)
74
  def show_photos():
75
  # ========== 上下文日志(保留,用于排查) ==========
76
+ print(f"\n【上下文】进程ID:{os.getpid()},线程ID:{threading.current_thread().ident},线程名:{threading.current_thread().name}")
77
  print(f"【调用栈】:{''.join(traceback.format_stack())}")
78
  # ========== 原有业务逻辑 ==========
79
  photos = ["廖荣全.jpg"]
 
81
  for photo in photos:
82
  print(f"已显示: {photo}")
83
 
84
+ # ========== 打印线程信息(根因定位) ==========
85
+ print("\n【所有线程详情(根因定位)】:")
86
+ for thread in threading.enumerate():
87
+ print(f"线程名:{thread.name},ID:{thread.ident},守护线程:{thread.daemon}")
 
 
 
 
 
88
 
89
+ # ====================== 主执行逻辑(保留__main__守护) ======================
90
+ if __name__ == '__main__':
91
  # 只执行一次调用
92
  show_photos()
93
 
94
+ # 可选:容器启动后,若需要清理计数文件(比如下次启动重新执行),可注释掉以下代码
95
+ # import atexit
96
+ # def clean_count_file():
97
+ # if os.path.exists(COUNT_FILE):
98
+ # os.remove(COUNT_FILE)
99
+ # atexit.register(clean_count_file)